| I see a use for the general idea of macros where the expansion uses some functions (or macros or whatever) that you might not even know had definitions, and where it doesn't make a difference if the macro call appears inside a 'let that rebinds the name of one of the functions the macroexpansion uses. This seems to be where some people go for "hygienic macros". But I see what seems like a very simple way to do it: First, make sure that function objects evaluate to themselves[1], as do macros, and special forms like if.[2] Second, arrange for macroexpansions to contain, not the names of the objects they use, but the objects themselves. For illustration, here is the definition of 'for in arc.arc: (mac for (v init max . body)
(w/uniq (gi gm)
`(with (,v nil ,gi ,init ,gm (+ ,max 1))
(loop (assign ,v ,gi) (< ,v ,gm) (assign ,v (+ ,v 1))
,@body))))
It becomes something like this: (mac for2 (v init max . body)
(w/uniq (gi gm)
`(,with (,v nil ,gi ,init ,gm (,+ ,max 1))
(,loop (,assign ,v ,gi) (,< ,v ,gm) (,assign ,v (,+ ,v 1))
,@body))))
Two things become clear. First, quasiquoting seems kind of stupid because almost every term has a comma or comma-at, and, actually, the only terms without commas are the constants nil and 1, which could be comma'd. I'm thinking about what the best syntax for this use case would be; likely quasiquote but where all the bare terms are evaluated and you need an explicit ' to turn that off. Raw list operations certainly look worse.The second thing is that "assign", being a special form in Arc, doesn't actually have a definition, so if I want to even macroexpand it for demonstration, I have to give it one. Thus: arc> (macex1 '(for2 i 1 10 prn.i))
Error: "_assign: undefined;\n cannot reference undefined identifier"
arc> (= assign 'ASSIGN-OBJECT)
ASSIGN-OBJECT
arc> (macex1 '(for2 i 1 10 prn.i))
; after manual prettifying...
(#(tagged mac #<procedure: with>)
(i nil gs3342 1 gs3343 (#<procedure:+> 10 1))
(#(tagged mac #<procedure: loop>)
(ASSIGN-OBJECT i gs3342)
(#<procedure:<> i gs3343)
(ASSIGN-OBJECT i (#<procedure:+> i 1))
prn.i))
The macroexpansion contains no references to the symbol '+, and so if the whole expression were '(let + - (for i 1 10 prn.i)), we'd expect the same behavior as without the let.[3]Now, this does look worse than the macroexpansion of arc.arc's for: (with (i nil gs1722 1 gs1723 (+ 10 1))
(loop
(assign i gs1722)
(< i gs1723)
(assign i (+ i 1))
prn.i))
But that could be addressed with better printing style. Try this: (#M:with (i nil gs3342 1 gs3343 (#F:+ 10 1))
(#M:loop
(#S:assign i gs3342)
(#F:< i gs3343)
(#S:assign i (#F:+ i 1))
prn.i))
It does make it more difficult for macroexpansions to be written in a machine-readable way (such that passing it to 'read yields an equivalent structure). Though, if you wanted to give a meaning to the above, "#M:<name>" could be a reader macro that returns "the object that the global variable 'name refers to (and possibly throw an exception if it's not a macro)". Also, in Common Lisp, reading gensyms like #:G366 back in causes their identities to diverge (each instance of #:G366 it reads is a new uninterned symbol), so I guess 'read-able macroexpansions are probably not a real use case anyway.Using this approach has other implications. The global references are resolved at macroexpansion time instead of runtime. For an interpreter of the type I describe, which handles first-class macros as a matter of course, this is no change at all. But for a compiler that hopes to expand the macros once, ahead of time (e.g. when the function whose body uses the macro is defined), it's trickier. What happens when one of those global references gets redefined? The dumbest approach is to re-expand all macros when that happens; I think some compiled systems explicitly say "macros might get expanded multiple times, don't do anything stupid". If some function modifies a global variable—let's say, a stack represented as a list—repeatedly, then... It might make sense for the macro-function to explicitly quote that global variable, so that it remains a variable reference in the macroexpansion. And maybe such a compiled system could warn if there is compiled code that, deterministically, redefines a variable that a macro-function references. Anyway, that's my thinking so far. Questions: 1. Have people done this already? 2. What do you all think of this? [1] "What happens when a Common Lisp macro returns a list whose car is a function? (Not the name of a function, mind you, but an actual function.) What happens is what you'd expect, in every implementation I've used. But the spec doesn't say anything about this." http://www.paulgraham.com/ilc03.html [2] At least from the perspective of an interpreter, it seems to me that 'if and 'fn should be bound to special objects, and that the interpreter, when evaluating a form like '(if x y z), should evaluate the car of the expression, discover that it evaluates to the if-object, and use that to decide how it handles the rest of the form. The expression should be equivalent to, e.g., '((car (list if)) x y z). Likewise, '(<an expression evaluating to the for macro> i 1 10 prn.i) should print the numbers from 1 to 10. First-class macros and special forms, in other words. The common case of macros with global names can be handled by an inlining compiler; as for unexpectedly getting a macro in the functional position at runtime, if it's really needed for optimizations, at worst you could have a runtime flag that turns that into an exception, and therefore the compiler is allowed to assume that dead variables won't end up being referenced by a surprise macroexpansion. [3] The interpreter will run (eval '(for i 1 10 prn.i) env), where env is an environment containing the "+ -> -" binding; then it'll take the car, look up 'for, obtain the macro it's bound to, then call the macro-function with the arglist '(i 1 10 prn.i), after which it'll evaluate the macro-function's body in its saved lexenv (which is probably nil) augmented by bindings for the arguments 'v, 'init, 'max, and 'body. |