Macro-Writing Macros
Of course, there’s no reason you should be able to take advantage of macros only when writing functions. The job of macros is to abstract away common syntactic patterns, and certain patterns come up again and again in writing macros that can also benefit from being abstracted away.
In fact, you’ve already seen one such pattern—many macros will, like the last version of do-primes
, start with a **LET**
that introduces a few variables holding gensymed symbols to be used in the macro’s expansion. Since this is such a common pattern, why not abstract it away with its own macro?
In this section you’ll write a macro, with-gensyms
, that does just that. In other words, you’ll write a macro-writing macro: a macro that generates code that generates code. While complex macro-writing macros can be a bit confusing until you get used to keeping the various levels of code clear in your mind, with-gensyms
is fairly straightforward and will serve as a useful but not too strenuous mental limbering exercise.
You want to be able to write something like this:
(defmacro do-primes ((var start end) &body body)
(with-gensyms (ending-value-name)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
and have it be equivalent to the previous version of do-primes
. In other words, the with-gensyms
needs to expand into a **LET**
that binds each named variable, ending-value-name
in this case, to a gensymed symbol. That’s easy enough to write with a simple backquote template.
(defmacro with-gensyms ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
Note how you can use a comma to interpolate the value of the **LOOP**
expression. The loop generates a list of binding forms where each binding form consists of a list containing one of the names given to with-gensyms
and the literal code (gensym)
. You can test what code the **LOOP**
expression would generate at the REPL by replacing names
with a list of symbols.
CL-USER> (loop for n in '(a b c) collect `(,n (gensym)))
((A (GENSYM)) (B (GENSYM)) (C (GENSYM)))
After the list of binding forms, the body argument to with-gensyms
is spliced in as the body of the **LET**
. Thus, in the code you wrap in a with-gensyms
you can refer to any of the variables named in the list of variables passed to with-gensyms
.
If you macro-expand the with-gensyms
form in the new definition of do-primes
, you should see something like this:
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body))
Looks good. While this macro is fairly trivial, it’s important to keep clear about when the different macros are expanded: when you compile the **DEFMACRO**
of do-primes
, the with-gensyms
form is expanded into the code just shown and compiled. Thus, the compiled version of do-primes
is just the same as if you had written the outer **LET**
by hand. When you compile a function that uses do-primes
, the code generated by with-gensyms
runs generating the do-primes
expansion, but with-gensyms
itself isn’t needed to compile a do-primes
form since it has already been expanded, back when do-primes
was compiled.
Another classic macro-writing MACRO: ONCE-ONLY
Another classic macro-writing macro is once-only
, which is used to generate code that evaluates certain macro arguments once only and in a particular order. Using once-only
, you could write do-primes
almost as simply as the original leaky version, like this:
(defmacro do-primes ((var start end) &body body)
(once-only (start end)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
However, the implementation of once-only
is a bit too involved for a blow-by-blow explanation, as it relies on multiple levels of backquoting and unquoting. If you really want to sharpen your macro chops, you can try to figure out how it works. It looks like 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)))))