Consider the following Haskell code. We will be striving to imitate it as closely as possible using an Arc macro.
union xs [] = xsFirst we will see how this function might traditionally be written.
union [] ys = ys
union xs@(x:xt) ys@(y:yt) | x < y = x : union xt ys
| y < x = y : union xs yt
| otherwise = x : union xt yt
(def union (< xs ys)As a useful side-note, Arc's let and with forms come with destructuring. This can save us a whole line here, as well as make the code more readable (no more cryptic cars and cdrs).
(if (no xs) ys
(no ys) xs
(with (x (car xs) xt (cdr xs)
y (car ys) yt (cdr ys))
(if (< x y) (cons x (union < xt ys))
(< y x) (cons y (union < xs yt))
(cons x (union < xt yt))))))
(def union (< xs ys)This isn't bad. If we were programming in the wild, we would leave well enough alone at this point. However, this is an experiment; we want it to look Haskellian, not Lispy.
(if (no xs) ys
(no ys) xs
(with ((x . xt) xs (y . yt) ys)
(if (< x y) (cons x (union < xt ys))
(< y x) (cons y (union < xs yt))
(cons x (union < xt yt))))))
We'll start with pcase a 10-line macro using almkglor's pattern matching library to yield something similar to Scala's match-case statement.
(def union (< xs ys)Switching from pcase to hcase, the outer parens in the patterns go away and equals signs separate the patterns from their corresponding expressions.
(pcase `(,xs ,ys)
(xs ()) xs
(() ys) ys
((x . xt) (y . yt))
(if (< x y) (cons x (union < xt ys))
(< y x) (cons y (union < xs yt))
(cons x (union < xt yt)))))
(def union (< xs ys)To bring more Haskell syntax into the mix, we want to be able to substitute [] for () and (x:xt) for (x . xt). The colon and the brackets have special meanings in Arc, but our macro can intercept them without too much difficulty.
(hcase `(,xs ,ys)
xs () = xs
() ys = ys
(x . xt) (y . yt) =
(if (< x y) (cons x (union < xt ys))
(< y x) (cons y (union < xs yt))
(cons x (union < xt yt)))))
(def union (< xs ys)Next, we can replace that pesky if statement with guard clauses.
(hcase `(,xs ,ys)
xs [] = xs
[] ys = ys
(x:xt) (y:yt) =
(if (< x y) (cons x (union < xt ys))
(< y x) (cons y (union < xs yt))
(cons x (union < xt yt)))))
(def union (< xs ys)The a single pipe | character can't be used here for technical reasons, so we'll have to make due with a slash /. A double pipe || could be used just as easily.
(hcase `(,xs ,ys)
xs [] = xs
[] ys = ys
(x:xt) (y:yt) / (< x y) = (cons x (union < xt ys))
/ (< y x) = (cons y (union < xs yt))
/ otherwise = (cons x (union < xt yt))))
Since the guard conditions are enclosed on either side by / and =, we can make the outer parens optional. The xs@(x:xt) constructs known as "as-patterns" are the next addition. Having these as-patterns, we can define a simple macro to bring hcase up to the level of function arguments.
(mac hdef (name . body)The result: Lo! In a dazzling display of engineering hubris, a Lisp dialect is dragged kicking and screaming into Haskell-like syntax. If you dare, you can take look at the definition of hcase in all its convoluted glory.
(let args (uniq)
`(def ,name ,args (hcase ,args ,@body))))
(hdef union
_ xs [] = xs
_ [] ys = ys
< xs@(x:xt) ys@(y:yt) / < x y = (cons x (union < xt ys))
/ < y x = (cons y (union < xs yt))
/ otherwise = (cons x (union < xt yt)))
Implementation note: Like most Lisps, Arc would read '(x : xs) as a list of three symbols, but '(x:xs) has only one. In order to behave properly, hcase must split apart the symbols in patterns: '(xs@(x:y:yt)) becomes '(xs @ (x : y : yt)) and so on.
2 comments:
You just proved me the value of Arcs' if macro so I'll make such wrapper macro for common lisp, if someone already didn't do it before me.
BTW Haskell pattern matching is very limiting, one sided, no repeat of avariable in a pattern.
Slobodan: Haskell has some of the best pattern matching around, so I was a bit confused by your remark -- then I saw that you had Prolog experience :)
Relative to Prolog, I understand why you would view it as limiting.
Post a Comment