-
Notifications
You must be signed in to change notification settings - Fork 46
Expand file tree
/
Copy path2020-03-12-visual-arrow-syntax.lhs
More file actions
380 lines (297 loc) Β· 14.2 KB
/
Copy path2020-03-12-visual-arrow-syntax.lhs
File metadata and controls
380 lines (297 loc) Β· 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
---
title: 'Visual Arrow Syntax'
description: 'Finally, better Arrow syntax, completely free of hacks'
tags: 'haskell'
featured: true
---
_Not to be taken seriously._
Haskell is great building at DSLs -- which are perhaps the ultimate
form of slacking off at work. Rather than actually doing the work your
manager tells you to, you can build DSLs to delegate this back to your
manager so you can focus on finally writing up that GHC proposal for
`MultilinePostfixTypeOperators` (which could have come in useful for this
blogpost).
So, we'll build a visual DSL that's so simple even your manager can use it!
[This blogpost is a literate Haskell file][lhs] so you can run it directly
in GHCi. Note that some code is located in a [second module] because of
compilation stage restrictions.
[lhs]: https://github.com/jaspervdj/jaspervdj/blob/master/posts/2020-03-12-visual-arrow-syntax.lhs
[second module]: https://github.com/jaspervdj/jaspervdj/blob/master/files/2020-03-12-demo.hs
Let's get started. We'll need a few language extensions -- not too much, just
enough to guarantee job security for the forseeable future.
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE GADTs #-}
> {-# LANGUAGE KindSignatures #-}
> {-# LANGUAGE LambdaCase #-}
> {-# LANGUAGE PolyKinds #-}
> {-# LANGUAGE TypeFamilies #-}
> {-# LANGUAGE TypeOperators #-}
> module Visual where
And then some imports, not much going on here.
> import qualified Codec.Picture as JP
> import qualified Codec.Picture.Types as JP
> import Control.Arrow
> import Control.Category
> import Control.Monad.ST (runST)
> import Data.Char (isUpper)
> import Data.Foldable (for_)
> import Data.List (sort, partition)
> import qualified Language.Haskell.TH as TH
> import Prelude hiding (id, (.))
All Haskell tutorials that use some form of dependent typing seem to start
with the `HList` type. So I suppose we'll do that as well.
> data HList (things :: [*]) where
> Nil :: HList '[]
> Cons :: x -> HList xs -> HList (x ': xs)
I think `HList` is short for hype list. There's a lot of hype around this
because it allows you to put even more types in your types.
We'll require two auxiliary functions for our hype list. Because of all the
hype, they each require a type family in order for us to even express their
types. The first one just takes the last element from a list.
> hlast :: HList (thing ': things) -> Last (thing ': things)
> hlast (Cons x Nil) = x
> hlast (Cons _ (Cons y zs)) = hlast (Cons y zs)
> type family Last (l :: [*]) :: * where
> Last (x ': '[]) = x
> Last (x ': xs) = Last xs
Readers may wonder if this is safe, since `last` is usually a partial function.
Well, it turns out that partial functions are safe if you type them using
partial type families. So one takeaway is that partial functions can just be
fixed by adding more partial stuff on top. This explains things like `Prelude`.
Anyway, the second auxiliary function drops the last element from a list.
> hinit :: HList (thing ': things) -> HList (Init (thing ': things))
> hinit (Cons _ Nil) = Nil
> hinit (Cons x (Cons y zs)) = Cons x (hinit (Cons y zs))
> type family Init (l :: [*]) :: [*] where
> Init (_ ': '[]) = '[]
> Init (x ': y ': zs) = x ': Init (y ': zs)
And that's enough boilerplate! Let's get right to it.
It's always good to pretend that your DSL is built on solid foundations. As I
alluded to in the title, we'll pick Arrows. One reason for that is that they're
easier to explain to your manager than Applicative (stuff goes in, other stuff
comes out, see? They're like the coffee machine in the hallway). Secondly,
they are less powerful than Monads and we prefer to keep that good stuff to
ourselves.
Unfortunately, it seems like the Arrow module was contributed by an operator
fetishism cult, and anyone who's ever done non-trivial work with Arrows now
has a weekly therapy session to talk about how `&&&` and `***` hurt them.
This is not syntax we want anyone to use. Instead, we'll, erm, _slightly_
bend Haskell's syntax to get something that is "much nicer" and "definitely
not an abomination".
We'll build something that appeals to both Category Theorists (for street
cred) and Corporate Managers (for our bonus). These two groups have many
things in common. Apart from talking a lot about abstract nonsense and
getting paid for it, both **love** drawing boxes and arrows.

Yeah, so I guess we can call this visual DSL a `Diagram`. The main drawback
of arrows is that they can only have a single input and output. This leads to a
lot of tuple abuse.
We'll "fix" that by having extra `ins` and `outs`. We are wrapping an arbitrary
`Arrow`, referred to as `f` in the signature:
> data Diagram (ins :: [*]) (outs :: [*]) f a b where
We can create a diagram from a normal arrow, that's easy.
> Diagram :: f a b -> Diagram '[] '[] f a b
And we can add another normal function at the back. No biggie.
> Then
> :: Diagram ins outs f a b -> f b c
> -> Diagram ins outs f a c
Of course, we need to be able to use our extra input and outputs. `Output`
wraps an existing `Diagram` and redirects the second element of a tuple to the
`outs`; and `Input` does it the other way around.
> Output
> :: Diagram ins outs f a (b, o)
> -> Diagram ins (o ': outs) f a b
> Input
> :: Diagram ins outs f a b
> -> Diagram (i ': ins) outs f a (b, i)
The hardest part is connecting two existing diagrams. This is really where
the magic happens:
> Below
> :: Diagram ins1 outs1 f a b
> -> Diagram (Init (b ': outs1)) outs2 f (Last (b ': outs1)) c
> -> Diagram ins1 outs2 f a c
Is this correct? What does it even mean? The answer to both questions is: "I
don't know". It typechecks, which is what really matters when you're doing
Haskell. And there's something about `ins` matching `outs` in there, yeah.
Concerned readers of this blog may at this point be wondering why we used
reasonable names for the constructors of `Diagram` rather than just operators.
Well, it's only because it's a GADT which makes this impossible. But fear
not, we can claim our operators back. Shout out to Unicode's [Box-drawing
characters]: they provide various charaters with thick _and_ thin lines.
This lets us do an, uhm, _super intuitive syntax_ where tuples are taken apart
as extra inputs/outputs, or reified back into tuples.
> (ββΊ) = Then
> l ββΊ r = Output l ββΊ r
> l β³βΊ r = (l ββΊ arr (\x -> (x, x))) ββΊ r
> l βΆβΊ r = Input l ββΊ r
> l ββΊ r = Output (Input l ββΊ arr (\x -> (x, x))) ββΊ r
> l β³ c = l β³βΊ arr (const c)
> l β r = Below l r
> l β§ r = Input l β r
> l β r = Input l ββΊ arr snd β r
> infixl 5 ββΊ, β³βΊ, ββΊ, βΆβΊ, ββΊ, β³
> infixr 4 β, β§, β
Finally, while we're at it, we'll also include an operator to clearly indicate
to our manager how our valuation will change if we adopt this DSL.
> (π) = Diagram
This lets us do the basics. If we start from regular Arrow syntax:
> horribleExample01 =
> partition isUpper >>> reverse *** sort >>> uncurry mappend
We can now turn this into:
> amazingExample01 =
> (π) (partition isUpper)ββΊreverseβ
> (π) sort βΆβΊ(uncurry mappend)
The trick to decrypting these diagrams is that each line in the source code
consists of an arrow where values flow from the left to the right; with possible
extra inputs and ouputs in between. These lines are then composed using a few
operators that use `Below` such as `β` and `β§`.
To improve readability even further, it should also be possible to add
right-to-left and top-to-bottom operators. I asked my manager if they wanted
these extra operators but they've been ignoring all my Slack messages since I
showed them my original prototype. Probably just busy?
Anyway, there are other simple improvements we can make to the visual DSL first.
Most Haskellers prefer nicely aligning things over producing working code,
so it would be nice if we could draw longer lines like `βββββ³ββΊ` rather than
just `β³βΊ`. And any Haskeller worth their salt will tell you that this is where
Template Haskell comes in.
Template Haskell gets a bad rep, but that's only because it is mostly misused.
Originally, it was designed to avoid copying and pasting a lot of code, which is
**exactly** what we'll do here. Nothing to be grossed out about.
> extensions :: Maybe Char -> String -> Maybe Char -> [String]
> extensions mbLeft operator mbRight =
> [operator] >>= maybe pure goR mbRight >>= maybe pure goL mbLeft
> where
> goL l op = [replicate n l ++ op | n <- [1 .. 19]]
> goR r op = [init op ++ replicate n r ++ [last op] | n <- [1 .. 19]]
> industryStandardBoilerplate
> :: Maybe Char -> TH.Name -> Maybe Char -> TH.Q [TH.Dec]
> industryStandardBoilerplate l name r = do
> sig <- TH.reify name >>= \case
> TH.VarI _ sig _ -> pure sig
> _ -> fail "no info"
> fixity <- TH.reifyFixity name >>= maybe (fail "no fixity") pure
> pure
> [ decl
> | name' <- fmap TH.mkName $ extensions l (TH.nameBase name) r
> , decl <-
> [ TH.SigD name' sig
> , TH.FunD name' [TH.Clause [] (TH.NormalB (TH.VarE name)) []]
> , TH.InfixD fixity name'
> ]
> ]
We can then invoke this industry standard boilerplate to extend and copy/paste
an operator like this:
`````haskell
$(industryStandardBoilerplate (Just 'β') '(ββΊ) (Just 'β'))
`````
We're now equipped to silence even the harshest syntax critics:
`````haskell
example02 =
(π) (partition isUpper)ββββΊ(reverse)ββ
(π) (sort)ββββββββββΆββΊ(uncurry mappend)
`````
Beautiful! If you've ever wondered what people mean when they say functional
programs "compose elegantly", well, this is what they mean.
`````haskell
example03 =
(π) (+1)ββ³ββΊ(+1)ββ
(π) (+1)βββββββΊaddββ
(π) addβββββΆββΊadd
where
add = uncurry (+)
`````
Type inference is excellent and running is easy. In GHCi:
`````
*Main> :t example03
example04 :: Diagram '[] '[] (->) Integer Integer
*Main> run example03 1
12
`````
Let's look at a more complicated example.
`````haskell
lambda =
(π) (id)ββββΊ(subtract 0.5)ββ³βββββββββΊ(< 0)βββββββββββ
(π) (subtract 0.5)ββββββββββΊ(add)ββΊ(abs)ββΊ(< 0.1)ββΆββββββββΊ(and)ββββββββ
(π) (swap)ββββΊ(* pi)βββΊ(sin)β³() β
(π) (* 2)βββββββββββββββΆββΊ(sub)ββΊ(abs)ββΊ(< 0.2)ββ§
(π) (or)ββΊ(bool bg fg)
where
add = uncurry (+)
sub = uncurry (-)
and = uncurry (&&)
or = uncurry (||)
fg = JP.PixelRGB8 69 58 98
bg = JP.PixelRGB8 255 255 255
`````
This renders everyone's favorite greek letter:
{width=30%}
Amazing! Math!
While the example diagrams in this post all use the pure function arrow `->`,
it is my duty as a Haskeller to note that it is really parametric in `f` or
something. What this means is that thanks to this famous guy called [Kleisli],
you can immediately start using this with `IO` in production. Thanks for
reading!
[Kleisli]: https://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Arrow.html#t:Kleisli
**Update**: [CarlHedgren](https://twitter.com/CarlHedgren) pointed out to me
that a similar DSL is provided by [Control.Arrow.Needle][needle]. However, that
package uses Template Haskell to just parse the diagram. In this blogpost, the
point of the exercise is to bend Haskell's syntax and type system to achieve
the notation.
[needle]: https://hackage.haskell.org/package/needle-0.1.0.1
Appendix 1: run implementation
------------------------------
The implementation of `run` uses a helper function that lets us convert
a diagram back to a normal `Arrow` that uses `HList` to pass extra inputs
and outputs:
> fromDiagram
> :: Arrow f => Diagram ins outs f a b
> -> f (a, HList ins) (b, HList outs)
We can then have a specialized version for when there's zero extra inputs
and outputs. This great simplifies the type signatures and gives us a
"normal" `f a b`:
> run :: Arrow f => Diagram '[] '[] f a b -> f a b
> run d = id &&& (arr (const Nil)) >>> fromDiagram d >>> arr fst
The definition for `fromDiagram` is as follows:
> fromDiagram (Diagram f) = f *** arr (const Nil)
> fromDiagram (Then l r) = fromDiagram l >>> first r
> fromDiagram (Output l) =
> fromDiagram l >>> arr (\((x, y), things) -> (x, Cons y things))
> fromDiagram (Input l) =
> arr (\(x, Cons a things) -> ((x, things), a)) >>>
> first (fromDiagram l) >>>
> arr (\((y, outs), a) -> ((y, a), outs))
> fromDiagram (Below l r) =
> fromDiagram l >>>
> arr (\(x, outs) -> (hlast (Cons x outs), hinit (Cons x outs))) >>>
> fromDiagram r
Appendix 2: some type signatures
--------------------------------
We wouldn't want these to get in our way in the middle of the prose, but GHC
complains if we don't put them somewhere.
> (β³βΊ) :: Arrow f => Diagram ins outs f a b -> f b c
> -> Diagram ins (b ': outs) f a c
> (ββΊ) :: Arrow f => Diagram ins outs f a (b, o) -> f b c
> -> Diagram ins (o ': outs) f a c
> (βΆβΊ) :: Diagram ins outs f a b -> f (b, i) c
> -> Diagram (i ': ins) outs f a c
> (ββΊ) :: Arrow f => Diagram ins outs f a b -> f (b, u) c
> -> Diagram (u ': ins) ((b, u) ': outs) f a c
> (β§) :: Diagram ins1 outs1 f a b
> -> Diagram (Init ((b, u) ': outs1)) outs2 f (Last ((b, u) ': outs1)) c
> -> Diagram (u ': ins1) outs2 f a c
Appendix 3: image rendering boilerplate
---------------------------------------
This uses a user-supplied `Diagram` to render an image.
> image
> :: Int -> Int
> -> Diagram '[] '[] (->) (Double, Double) JP.PixelRGB8
> -> JP.Image JP.PixelRGB8
> image w h diagram = runST $ do
> img <- JP.newMutableImage w h
> for_ [0 .. h - 1] $ \y ->
> for_ [0 .. w - 1] $ \x ->
> let x' = fromIntegral x / fromIntegral (w - 1)
> y' = fromIntegral y / fromIntegral (h - 1) in
> JP.writePixel img x y $ run diagram (x', y')
> JP.freezeImage img
[Box-drawing characters]: https://en.wikipedia.org/wiki/Box-drawing_character