Skip to content

LISP - Macro Writing Style

Macros are a powerful tool for meta-programming, but their power comes with responsibility. Writing clean, predictable, and maintainable macros is crucial for a healthy codebase. This tutorial covers the essential best practices for professional macro development.

Hygiene is the most critical concept in macro writing. An unhygienic macro introduces internal variables that can accidentally clash with variables in the code where the macro is used. This is called variable capture and leads to unpredictable bugs.

The gensym function generates a new, unique symbol that is guaranteed not to exist anywhere else in the code. By using gensym for all internal variables within a macro, you can ensure there will never be a name collision.

;; A macro to swap the values of two places (variables).
(defmacro swap (x y)
"Swaps the values of two places X and Y."
(let ((temp-var (gensym "TEMP"))) ; Create a unique symbol for the temporary variable.
`(let ((,temp-var ,x))
(setf ,x ,y)
(setf ,y ,temp-var))))
;; --- Usage ---
(let ((a 5) (b 10))
(format t "Before: a=~a, b=~a~%" a b)
(swap a b)
(format t "After: a=~a, b=~a~%" a b))
;; This works even if a user has a variable named TEMP.
(let ((a 1) (temp 99))
(format t "Before: a=~a, temp=~a~%" a temp)
(swap a temp)
(format t "After: a=~a, temp=~a~%" a temp))

Building code by manually constructing lists (e.g., (list 'let ...) ) is verbose and error-prone. Quasiquotation (the backquote syntax) is the standard and most readable way to construct code templates inside a macro.

  • Backquote ( ): Creates a code template. Most of it is treated as literal data.
  • Comma (,): ‘Unquotes’ the following expression, evaluating it and inserting the result into the template.
  • Comma-At (,@): ‘Splices’ the following expression. It must evaluate to a list, and the list’s elements are inserted into the template as if they were typed there directly.

When a macro behaves unexpectedly, your most valuable tool is macroexpand-1. This function takes a macro form and shows you the code it expands into, one level deep. This allows you to inspect the generated code and spot errors.

;; At the REPL, quote the form you want to inspect.
CL-USER> (macroexpand-1 '(swap my-var other-var))
;; The output will look something like this:
(LET ((#:TEMP1057 MY-VAR))
(SETF MY-VAR OTHER-VAR)
(SETF OTHER-VAR #:TEMP1057))
T

The #:TEMP1057 is the printed representation of the uninterned symbol created by gensym. Seeing this confirms our macro is hygienic.

Pitfall: Evaluating Arguments Multiple Times

Section titled “Pitfall: Evaluating Arguments Multiple Times”

A naive macro might evaluate its arguments more than once, leading to incorrect behavior if the arguments have side effects.

;; BAD macro: `x` is evaluated twice!
(defmacro bad-square (x)
`(* ,x ,x))
(let ((i 2))
(bad-square (incf i))) ; Expands to (* (incf i) (incf i)), which is 3 * 4 = 12!
;; GOOD macro: evaluate the argument once and store it in a temporary variable.
(defmacro good-square (x)
(let ((val (gensym)))
`(let ((,val ,x))
(* ,val ,val))))
(let ((i 2))
(good-square (incf i))) ; Correctly evaluates to 3 * 3 = 9

Always include a documentation string in your macros. Explain:

  • What the macro does.
  • The syntax it introduces.
  • Any non-obvious evaluation rules.
  • A clear example of its usage.