| This is going to be a long, mind-bending story about macro-generating macros. Grab a chair. I've been interested in generic functions for a long time, the ability to say: def (len x) :case (isa queue x)
..
(The syntax is irrelevant, though I like this form. The crux is to be able to insert behaviors to names in multiple places, allowing us to separate concerns. I can define len in one place, and then extend it for some new case far away in space and time. Inspired by https://en.wikipedia.org/wiki/Ruby_%28programming_language%29#Open_classes)--- At some point I realized that I could also similarly extend macros (http://arclanguage.org/item?id=13790; http://arclanguage.org/item?id=17233): mac (with x ... rest) :case (x = 'infile)
..
with infile "x"
(read)
=> 123 # reads from file "x"
The way this works is that this: mac (name params) :case cond
..
expands to this: let $old name
mac (name params)
if cond
..
`(,$old ,params)
Got all that? Good. No? Skip the rest and leave a question.--- So things stood for a long long time (since http://arclanguage.org/item?id=13790). But then a few days ago I was trying to build a data structure that needed to override a mutator: mac (push x s) :case (isa stack s)
`(push ,x (rep ,s))
And it just didn't work. Eventually I figured out the problem: I'd setup conditions to evaluate at macroexpansion time, when only the names of args are available, not values. Basically, the above expression was expanding to this: let $old push
mac (push x s)
if (isa stack s)
`(push ,x (rep ,s))
`(,$old ,x ,s)
But I needed it to expand to this: let $old push
mac (push x s)
`(if (isa stack ,s)
(push ,x (rep ,s))
(,$old ,x ,s))
In other words, I needed to implicitly generate a backquote. So I spent several hours trying to make this work, with weird syntax where backquotes didn't match unquotes: mac (push x s) :case (isa stack ,s)
(push ,x (rep ,s))
..but something would always break. Eventually I realized why: I need room to operate on the names before starting on the backquote. Imagine something like this: mac (foo n)
with (a ..
b ..)
`(bar ,a ,b)
You can't handle this if you're implicitly generating the backquote. You need to control what happens at macroexpansion time and what happens at eval time.--- Ok, so what can we try next? Perhaps this means push shouldn't be a macro? Hey, wart has selective evaluation. Do mutation operators really need to be macros? What if I changed the original definition of push from this: mac (push x seq)
`(<- ,seq (cons ,x ,seq))
to this: def (push x ('name | seq))
(<- name (cons x seq))
(Wart has the '|' operator for haskell-style as-expressions, so I can here use both the name of the second arg and its value: http://arclanguage.org/item?id=16970)But no, that doesn't work because the binding to name doesn't exist in this lexical scope. We do need a macro. --- Ok, next idea: two backquoted expressions. I'm gonna perform extricate the backquote in: let $old push
mac (push x s)
`(if (isa stack ,s)
(push ,x (rep ,s))
(,$old ,x ,s))
Macros aren't primitives in wart. Inspired by Kernel, they're built out of eval and caller_scope. So this: mac (foo n)
`(+ ,n 1)
is identical to this: foo <- (fn (n) (eval `(+ ,n 1) caller_scope))
I'm gonna pull this transform into the condition expression above, so that push now code-generates to: let $old push
mac (push x s)
if (eval `(isa stack ,s) caller_scope)
`(push ,x (rep ,s))
`(,$old ,x ,s))
Now the backquotes and quotes all line up, suggesting we might be on the right track: mac (push x s) :qcase `(isa stack ,s)
`(push ,x (rep ,s))
:qcase is my ad hoc name for "eval-time case that needs to operate on the values of args".--- But that isn't quite right; there's one last wrinkle. The caller_scope inside the macro body is different from the caller_scope after returning from the macro. The expansion for mac is: <- ,name (fn ',params
(eval ((fn() ,@body))
caller_scope))
The eval and the fn seem to overlay their own values to caller_scope, which we can tunnel through by: ((caller_scope 'caller_scope) 'caller_scope)
(Scopes are just tables keyed by symbol names.)So the final expansion is: let $old push
mac (push x s)
if (eval `(isa stack ,s) (macro_caller_scope)) # expression above
`(push ,x (rep ,s))
`(,$old ,x ,s))
---So what do people think of all this? Hack piled on hack, or useful? I'm still not convinced I fully understand why eval and fn each add an indirection to caller_scope. Fortunately the definition of (macro_caller_scope) seems stable. If it needed to change everytime I redefined mac, that would suck. (Yes, I extend mac just like any other macro. Several times.) That it's stable increases confidence that there isn't some further use case I haven't considered that'll bring this whole edifice crumbling down. But even if there isn't, I'm starting to question if lisp is indeed the hundred-year programming model. I'm starting to feel at the end of my tether, at the limits of what macroexpansion can cleanly support. If you want to read more, here's how I extend mac: https://github.com/akkartik/wart/blob/5b093c852c/047generic.wart. As always, if you want to play with these code snippets: $ git clone http://github.com/akkartik/wart
$ cd wart
$ ./wart
Feel free to ask me more questions, either here or over email (see my profile). |