At the end of Ch 8 in Practical Common Lisp, Peter Seibel presents the once-only
macro. Its purpose is to mitigate a number of subtle problems with variable evaluation in user-defined macros. Note I'm not trying to understand at this point how this macro works, as in some other posts, but just how to use it properly:
(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 following is a sample (incorrect) contrived macro that attempts to exhibit several variable evaluation problems. It purports to iterate over a range of integers by some delta, returning the range:
(defmacro do-range ((var start stop delta) &body body)
"Sample macro with faulty variable evaluations."
`(do ((,var ,start (+ ,var ,delta))
(limit ,stop))
((> ,var limit) (- ,stop ,start))
,@body))
For example, (do-range (i 1 15 3) (format t "~A " i))
should print 1 4 7 10 13
and then return 14
.
The problems include 1) potential capture of the second occurrence of limit
, since it occurs as a free variable, 2) potential capture of the initial occurrence of the bound variable limit
, since it occurs in an expression along with other variables appearing in the macro parameters, 3) out of order evaluation, since delta
will be evaluated before stop
, even though stop
appears before delta
in the parameter list, and 4) multiple variable evaluations, since stop
and start
are evaluated more than once. As I understand it, once-only
should fix these problems:
(defmacro do-range ((var start stop delta) &body body)
(once-only (start stop delta limit)
`(do ((,var ,start (+ ,var ,delta))
(limit ,stop))
((> ,var limit) (- ,stop ,start))
,@body)))
However, (macroexpand '(do-range (i 1 15 3) (format t "~A " i)))
complains about limit
being an unbound variable. If I switch instead to with-gensyms
, which should take care of problems 1 & 2 above only, the expansion proceeds without incident.
Is this an issue with the once-only
macro? And does once-only
really solve all the problems outlined above (and perhaps others)?