14.5.5 How Many Times is the Macro Expanded
Occasionally problems result from the fact that a macro call is expanded each time it is evaluated in an interpreted function, but is expanded only once (during compilation) for a compiled function. If the macro definition has side effects, they will work differently depending on how many times the macro is expanded.
Therefore, you should avoid side effects in computation of the macro expansion, unless you really know what you are doing.
One special kind of side effect can’t be avoided: constructing Lisp objects. Almost all macro expansions include constructed lists; that is the whole point of most macros. This is usually safe; there is just one case where you must be careful: when the object you construct is part of a quoted constant in the macro expansion.
If the macro is expanded just once, in compilation, then the object is constructed just once, during compilation. But in interpreted execution, the macro is expanded each time the macro call runs, and this means a new object is constructed each time.
In most clean Lisp code, this difference won’t matter. It can matter only if you perform side-effects on the objects constructed by the macro definition. Thus, to avoid trouble, avoid side effects on objects constructed by macro definitions. Here is an example of how such side effects can get you into trouble:
(defmacro empty-object ()
(list 'quote (cons nil nil)))
(defun initialize (condition)
(let ((object (empty-object)))
(if condition
(setcar object condition))
object))
If initialize
is interpreted, a new list (nil)
is constructed each time initialize
is called. Thus, no side effect survives between calls. If initialize
is compiled, then the macro empty-object
is expanded during compilation, producing a single constant (nil)
that is reused and altered each time initialize
is called.
One way to avoid pathological cases like this is to think of empty-object
as a funny kind of constant, not as a memory allocation construct. You wouldn’t use setcar
on a constant such as '(nil)
, so naturally you won’t use it on (empty-object)
either.