Reputation: 289
I'm learning common lisp. I have written a version of the once-only
macro, which suffers from an unusual variable capture problem.
My macro is this:
(defmacro my-once-only (names &body body)
(let ((syms (mapcar #'(lambda (x) (gensym))
names)))
``(let (,,@(mapcar #'(lambda (sym name) ``(,',sym ,,name))
syms names))
,(let (,@(mapcar #'(lambda (name sym) `(,name ',sym))
names syms))
,@body))))
The canonical version of only-once
is this:
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for n in names collect (gensym))))
`(let (,@(loop for g in gensyms collect `(,g (gensym))))
`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
,@body)))))
The difference, as far as I can tell, is that the canonical version generates new symbols for every expansion of the macro using only-once
. For example:
CL-USER> (macroexpand-1 '(once-only (foo) foo))
(LET ((#:G824 (GENSYM)))
`(LET (,`(,#:G824 ,FOO))
,(LET ((FOO #:G824))
FOO)))
T
CL-USER> (macroexpand-1 '(my-once-only (foo) foo))
`(LET (,`(,'#:G825 ,FOO))
,(LET ((FOO '#:G825))
FOO))
T
The variable my macro uses to store the value of foo
is the same for every expansion of this form, in this case it would be #:G825
. This is akin to defining a macro like the following:
(defmacro identity-except-for-bar (foo)
`(let ((bar 2))
,foo))
This macro captures bar
, and this capture manifests when bar
is passed to it, like so:
CL-USER> (let ((bar 1))
(identity-except-for-bar bar))
2
However, I cannot think of any way to pass #:G825
to a macro that uses my-only-once
so that it breaks like this, because the symbols gensym
returns are unique, and I cannot create a second copy of it outside of the macro. I assume that capturing it is unwanted, otherwise the canonical version wouldn't bother adding the additional layer of gensym
. How could capturing a symbol like #:G826
be a problem? Please provide an example where this capture manifests.
Upvotes: 4
Views: 332
Reputation: 58598
We can demonstrate a behavioral difference between my-once-only
and once-only
:
Let's store our test form in a variable.
(defvar *form* '(lexalias a 0 (lexalias b (1+ a) (list a b))))
This test form exercises a macro called lexalias
, which we will define in two ways. First with once-only
:
(defmacro lexalias (var value &body body)
(once-only (value)
`(symbol-macrolet ((,var ,value))
,@body)))
(eval *form*) -> (0 1)
Then with my-once-only
:
(defmacro lexalias (var value &body body)
(my-once-only (value)
`(symbol-macrolet ((,var ,value))
,@body)))
(eval *form*) -> (1 1)
Oops! The problem is that under my-once-only
, both a
and b
end up being symbol-macrolet
aliases for exactly the same gensym
; the returned expression (list a b)
ends up being something like (list #:g0025 #:g0025)
.
If you're writing a macro-writing helper that implements once-only evaluation, you have no idea how the symbol is going to be used by the code which calls the macro, whose author uses your once-only tool. There are two big unknowns: the nature of the macro and of its use.
As you can see, if you don't make fresh gensyms, it will not work correctly in all conceivable scenarios.
Upvotes: 5