Skip to content

LISP - Macros

Macros are one of the most powerful and defining features of Common Lisp. They allow you to extend the language’s syntax itself. A common saying is that macros are code that writes code.

Unlike a function, which operates on values at runtime, a macro is a transformation function that takes Lisp code (s-expressions) as input and produces new Lisp code as output at compile time. This new code is then compiled and executed in place of the original macro call.

You define a macro using the defmacro special operator. Its syntax is similar to defun:

(defmacro macro-name (parameter-list)
"Optional documentation string."
body-of-macro)

The body of the macro must return a Lisp form (a piece of code) that will replace the macro call.

Let’s create a macro with-logging that prints a message before and after executing a block of code. This is something a function cannot do without changing how the code is called.

To build code inside a macro, we use the backquote ( ) syntax. It acts like a template. We use a comma (,) to evaluate and insert a value into this template, and comma-at (,@) to splice a list of values.

(defmacro with-logging ((&key (stream '*standard-output*))
&body body)
"Executes BODY, printing a log message before and after."
`(progn
(format ,stream "--- Entering logged section ---")
,@body
(format ,stream "--- Exiting logged section ---")))
;; Now, let's use our new syntax
(with-logging ()
(print "Doing some work...")
(let ((x 10) (y 20))
(format t "The sum is ~a~%" (+ x y))))

When you execute this, Lisp first expands the macro into the progn form and then runs the resulting code:

--- Entering logged section ---
"Doing some work..."
The sum is 30
--- Exiting logged section ---

The &body keyword in the parameter list collects all subsequent forms into a single list called body. The macro body then constructs a progn block (which evaluates expressions sequentially). The ,@body splices the list of forms from the body variable directly into the progn block.

Common Lisp has loop, but let’s define our own while loop to demonstrate how macros can create new control flow structures.

(defmacro while (test &body body)
"Executes BODY as long as TEST evaluates to true."
`(loop while ,test
do (progn ,@body)))
;; Use our new `while` loop
(let ((counter 3))
(while (> counter 0)
(format t "Counter is ~a~%" counter)
(decf counter)))

This code expands into a standard loop macro call and produces the expected result:

Counter is 3
Counter is 2
Counter is 1
  • To create new syntax or control flow structures: As seen with with-logging and while.
  • To control evaluation: When you need to prevent arguments from being evaluated, or control how many times they are evaluated (e.g., short-circuiting logical operators like and and or).
  • To reduce boilerplate code at compile time: Writing code that generates repetitive, but slightly different, definitions.
  • Guideline: If you can solve a problem with a regular function, use a function. Reach for a macro only when a function is insufficient.