4.9 Macro-Writing Macros

This package includes two classic Common Lisp macro-writing macros to help render complex macrology easier to read.

Macro: cl-with-gensyms names… body

This macro expands to code that executes body with each of the variables in names bound to a fresh uninterned symbol, or gensym, in Common Lisp parlance. For macros requiring more than one gensym, use of cl-with-gensyms shortens the code and renders one’s intentions clearer. Compare:

(defmacro my-macro (foo)
  (let ((bar (gensym "bar"))
        (baz (gensym "baz"))
        (quux (gensym "quux")))
    `(let ((,bar (+ …)))
       …)))

(defmacro my-macro (foo)
  (cl-with-gensyms (bar baz quux)
    `(let ((,bar (+ …)))
       …)))
Macro: cl-once-only ((variable form)…) body

This macro is primarily to help the macro programmer ensure that forms supplied by the user of the macro are evaluated just once by its expansion even though the result of evaluating the form is to occur more than once. Less often, this macro is used to ensure that forms supplied by the macro programmer are evaluated just once.

Each variable may be used to refer to the result of evaluating form in body. cl-once-only binds each variable to a fresh uninterned symbol during the evaluation of body. Then, cl-once-only wraps the final expansion in code to evaluate each form and bind the result to the corresponding uninterned symbol. Thus, when the macro writer substitutes the value for variable into the expansion they are effectively referring to the result of evaluating form, rather than form itself. Another way to put this is that each variable is bound to an expression for the (singular) result of evaluating form.

The most common case is where variable is one of the arguments to the macro being written, so (variable variable) may be abbreviated to just variable.

For example, consider this macro:

(defmacro my-list (x y &rest forms)
  (let ((x-result (gensym))
        (y-result (gensym)))
    `(let ((,x-result ,x)
           (,y-result ,y))
       (list ,x-result ,y-result ,x-result ,y-result
             (progn ,@forms))))

In a call like (my-list (pop foo) …) the intermediate binding to x-result ensures that the pop is not done twice. But as a result the code is rather complex: the reader must keep track of how x-result really just means the first parameter of the call to the macro, and the required use of multiple gensyms to avoid variable capture by (progn ,@forms) obscures things further. cl-once-only takes care of these details:

(defmacro my-list (x y &rest forms)
  (cl-once-only (x y)
    `(list ,x ,y ,x ,y
           (progn ,@forms))))