In my last post I gave a guided tour of using Gazelle's module
system, and while I mentioned that one can use define-macro+ to
define an external macro, I did not provide an example. Turns out
that macros and module systems interact in ways that require some
thought. This post describes, via an example, how Gazelle attempts to
resolve this tension.
The Surface Problem
Suppose we want to add a type safe delay operation to Javascript. A
not typesafe delay operation, the lambda with no arguments
containing the expression we want to delay, presents itself
immediately, but such delays are not easily distinguishable from
regular functions, and we may wish to delay functions in certain
contexts where we wish to accept functions, delayed functions, or other
kinds of delayed things. Which is to say that we want to have a new
class with its own prototype we can use for dispatching which
implements delay.
(module ()
(define (Delay fun-value)
(set! this.function fun-value)
this)
(set! Delay.prototype.force
(lambda () (this.function))))
So far so good, now Delay objects are instanceof Delay. We can
dispatch with pattern matching, using the instanceof pattern, for instance.
So what about some syntactic sugar? As it stands, we construct a new
Delay like so:
(new Delay (lambda () some-expression))
But this is a bit redundant - delays should really be syntatically like a lambda without an argument list, eg:
(delay some-expression)
Luckily, we have a Lisp, so we are tempted to write the following macro:
(define-macro+ delay (expr)
`(new Delay (lambda () ,expr)))
This expression defines an external macro called delay which appears
to expand in a straightforward manner to an appropriate invocation of
new. But this is unfortunately not the case ...
What's in a Name?
So what is wrong with our macro, above? Well, if we use that macro like this:
(require (("delay/delay" :all))
(var d (delay 10))
(d.force))
We are in the clear. But if we are a bit more careful/clever:
(require (("delay/delay" (:as ($ delay))))
(var d ($ 10))
(d.force))
Where the require form now imports only the delay macro, and
renames it $, we will get an error that there is no constructor
corresponding to Delay, because we haven't imported any such binding
from the delay module. An error immediately on invocation of new is
the best outcome, in fact: if some other value is floating around
bound to Delay, we might have to wait until a very confusing moment
to find out there is a problem.
The problem is that macro expansion takes place in the module the macro is expanded in, and symbols in a macro expansion refer to symbols in that module, not the symbols in the module where the macro was defined.
Of course if your macros never want to "capture", then you don't
encounter this problem. For instance, we could have written delay
like this instead:
(define-macro+ delay (constructor expr)
`(new ,constructor (lambda () ,expr)))
Where we require the user to pass the constructor, presumably Delay,
to the macro to ensure that it expands correctly. I felt that this
was particularly ridiculous in this instance, because it reduces
delay to an alias for new with the restriction of a single
argument.
The Better Solution
The other alternative is to somehow allow the user to qualify which value or macro their macro expansions refer to. In Gazelle, you do this in the following way:
(define-macro+ delay (expr)
`(new (from "delay/delay" Delay) (lambda () ,expr)))
Where we use the special form from to refer to values from a
specific module, regardless of where the macro is expanded. Indeed,
from can be used in ordinary code to refer to module level values at
any time. Only public module objects and macros can be referred to.
So how does this solution stack up?
The Good: Obviously the ability to refer to specific values from specific modules despite the expansion environment is almost required for meaningful macros.
The Bad: However, resolving the issue of macro hygiene at only module-level granularity falls pretty far short of the nicer properties of hygienic macros or the implicit properties of Common Lisp Style macros.
The Ugly: You've got to remember to qualify your macro expansions when you want the module behavior, AND the
fromsyntax incurs a slight run time penalty for run-time values, because they are looked up viarequire.jsat each use. Presumably, this lookup is cached inrequire.jsand so should not be a big penalty. Still!
Conclusions
The solution outlined above is a reasonable compromise. The implementation of Gazelle, which uses the Emacs Lisp Reader, would have to be significantly more complex to support full macro hygiene (also, I don't yet know how to do that).
The important thing is that this solution enables most of what you want for certain kinds of macros. Probably in the next iteration of Gazelle, I'll think of something a little nicer.
No comments:
Post a Comment