One of the most common mistakes made when writing macros is evaluating one of the arguments multiple times. Not only can this be inefficient, but when side effects are involved, it leads to quirky behavior. Take a macro square, which simply squares its argument (in reality one would use a function to do this):
(defmacro square (x) `(* ,x ,x))
The above implementation is buggy. Why? Because the x argument is evaluated twice. To see why this is a bad thing, check out the following code:
(square (incf a))
The above winds up expanding into:
(* (incf a) (incf a))
Which is buggy since it increments a twice. A way to fix this problem is to bind the value of x to a gensym, and then use that gensym throughout the rest of the macro. Here is a bug free definition of square that uses with-gensyms:
(defmacro square (x) (with-gensyms (gx) `(let ((,gx ,x)) (* ,gx ,gx))))
Is there a way to automate this? Yes, there is, by using a macro called once-only. Once-only is a relatively complicated macro, but it eliminates lots of boilerplate code. Once-only takes a list of expressions, generally arguments to a macro, and makes sure they are evaluated only once in the final macro expansion. Here is an implementation of once-only based on the one from Practical Common Lisp:
(defmacro once-only ((&rest names) &body body) (let ((gensyms (loop for n in names collect (gensym)))) `(with-gensyms (,@gensyms) `(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)))))
In order to explain how once-only works, I’m first going to show how to rewrite square using it. From there I will show what square looks like after once-only has been expanded. After that I will show what the macro expansion of square looks like. Finally, I will give an explanation as to what is going on. If you are reading on a computer, I strongly recommed you open this page in another window so you can follow along with the code and the explanation at the same time. Here is an implementation of square that uses once-only:
(defmacro square (x) (once-only (x) `(* ,x ,x)))
Here is what square looks like after once-only has been expanded inline:
(defmacro square (x) (with-gensyms (#:g830) `(let (,`(,#:g830 ,x)) ,(let ((x #:g830)) `(* ,x ,x)))))
So a usage of square such as the following:
(square (incf x))
will wind up looking like the code below after macro expansion.
(let ((#:g831 (incf x))) (* #:g831 #:g831))
So what the heck is going on? In line 2 of once-only, it creates a list of gensyms, one for each of the expressions that should only be evaluated once. We then take these gensyms and on line 3, generate code that will bind them to fresh gensyms. That generated code becomes line 2 of square after once-only has been expanded. We need to do this because we are writing a macro that writes a macro or code that writes code that writes code. So, after once-only has been expanded, square’s body will contain a use of with-gensyms which will bind a bunch of gensyms to new gensyms every time square is ran. These fresh gensyms will eventually be the ones used to store the value of the expressions we want to be evaluated once only.
Now for lines 4-6. By using the double backquote, this code generates code that will generate code that will be part of the expansion of square. Lines 4-6 of once-only become line 3 of the definition of square, which becomes line 1 of the expansion of square. Basically the little segment
says to generate code that will generate code (double backquote), that will be a list containing the value of the value of g, and the value of the value of n. The value of g will be one of the gensyms we created in once-only. From line 3 of square after once-only has been expanded, we see that this gensym was #:g830. The value of #:g830 will be another gensym, whatever it was bound to by with-gensyms. From the code will can see that this gensym was #:g831. The value of n will be one of the arguments to once-only. From the original code for square we see that the only argument to once-only is x. Then the value of x, or the value of the value of n, will be whatever is passed as the argument to the square macro, in this case (incf x). Ultimately the code looks like this as it goes through the multiple expansions:
``(,,g ,,n) => `(,#:g830 ,x) => (#:g831 (incf x))
Lines 4-6 take a list of expressions similar to those in the middle of the above process, splices them into a let by using the comma-at, then evaluates each one of them by using the comma in order to evaluate them once more. This works because the single comma in ,,@ actually applies to every element in the spliced list. Here is an example that demonstrates this:
``(,,@ '(x y z)) => `(,x ,y ,z)
Then on line 3 of square after once-only has been expanded, we wind up with the comma followed by a backquote which wind up canceling each other out. So this is how lines 4-6 of once-only get us line 3 of square which then gives us line 1 of the expansion of square.
Now for lines 7-10 of once-only. These lines generate lines 4 and 5 of the code for square after once-only has been expanded. All these lines do is generate code that will bind the given names to the gensyms that will contain their values at runtime. In this case we want to bind x to the gensym #:g831. Since the value of #:g830 is #:g831, we can just bind x to the value of #:g830. Then we just evaluate the body in this environment. By doing this, we bind x to an expression that will give us the same value as the expression previously contained in x! And that is how once-only ultimately works. In the expansion of square, we bind #:g831 to the value of (incf x). Then we bind x to #:g831 so any where we insert the expression x, we get #:g831, a gensym which is bound to the value of the expression that was initially bound to x, but only evaluated once.
Ultimately, once-only is a fairly useful macro. Like with-gensyms it is a utility for writing other macros. Once-only greatly reduces boiler plate and complexity in cases where it is used. It is because of these reasons once-only is one of the most popular macros out there.