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)?
The ONCE-ONLY macro
To get rid of a warning that N
is unused, I would change the macro to:
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for nil in names collect (gensym))))
; changed N to NIL, NIL is ignored
`(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 purpose of this macro is to make sure that expressions are only evaluated once and in a defined order. For that it will introduce new uninterned variables and will bind the evaluation results to those. Inside he macro the new variables are available. The macro itself is provided to make writing macros easier.
Using ONCE-ONLY in DO-RANGE
Your example use of ONCE-ONLY
:
(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)))
Why is there LIMIT
in the once-only
list? limit
is undefined there. LIMIT
is used inside the ONCE-ONLY
form as a symbol, but outside there is no binding.
ONCE-ONLY
expects that the list of names is a list of symbols and that these names are bound to forms. In your case limit
is a symbol, but it is undefined.
We need to remove limit
from the list of names:
(defmacro do-range ((var start stop delta) &body body)
(once-only (start stop delta)
`(do ((,var ,start (+ ,var ,delta))
(limit ,stop))
((> ,var limit) (- ,stop ,start))
,@body)))
Now, what to do about LIMIT
? Given that once-only
provides bindings for the names, including for STOP
, we can eliminate the symbol LIMIT
and replace its use with ,stop
:
(defmacro do-range ((var start stop delta) &body body)
(once-only (start stop delta)
`(do ((,var ,start (+ ,var ,delta)))
((> ,var ,stop) (- ,stop ,start))
,@body)))
Example:
CL-USER 137 > (pprint
(macroexpand
'(do-range (i 4 10 2)
(print i))))
(LET ((#1=#:G2170 4)
(#3=#:G2171 10)
(#2=#:G2172 2))
(DO ((I #1# (+ I #2#)))
((> I #3#) (- #3# #1#))
(PRINT I)))