Lisp Macros

Macros are for transformation. Functions are for computation.

A first example

In Lisp we have an if expression.

(bind ((x 3))
  (if (>= x 0)
      'non-negative
      'negative))

We also have a cond expression.

(bind ((x 3))
  (cond ((> x 0) 'positive)
        ((< x 0) 'negative)
        ((= x 0) 'zero)))

But what if Lisp didn't have a cond expression? Let's write our own kond expression, as a Lisp macro.

(defmacro kond (&rest clauses)
  (if (null clauses)
      nil
      (let ((clause (first clauses))
            (rest   (rest clauses)))
        (if (eq (first clause) 't)
            `(progn ,@(rest clause))
            `(if ,(first clause)
                 (progn ,@(rest clause))
                 (kond ,@rest))))))

Let's first check that it works.

(bind ((x 3))
  (kond ((> x 0) 'positive)
        ((< x 0) 'negative)
        ((= x 0) 'zero)))

What is happening here?

First let's try kond with no clauses

(kond)

That gives us nil. We can see how that happens from the first two lines of the definition of kond

(defmacro kond (&rest clauses)
(if (null clauses) ;
nil ;
(let ((clause (first clauses)) (rest (rest clauses))) (if (eq (first clause) 't) `(progn ,@(rest clause)) `(if ,(first clause) (progn ,@(rest clause)) (kond ,@rest))))))

Now let's call kond with an otherwise clause, using t.

(kond (t 'default))

How does this work in the definition of kond?

(defmacro kond (&rest clauses)
  (if (null clauses)
      nil
(let ((clause (first clauses)) ;
(rest (rest clauses))) ;
(if (eq (first clause) 't) ;
`(progn ,@(rest clause)) ;
`(if ,(first clause) (progn ,@(rest clause)) (kond ,@rest))))))

We see that

  1. the else clause of the if is triggered,
  2. clauses is broken up into its first and rest,
  3. the variable clause is bound to (t 'default)
  4. the then clause of the second if is triggered, and
  5. `(progn ,@(rest clause)) is returned.

When we fill in that template we see

  • clause is bound to (t 'default)
  • so (rest clause) is '(default)
  • which is spliced in to yield 'default
  • which is evaluated to give 'default

Finally let's call kond with three clauses.

(bind ((x 3))
  (kond ((> x 0) 'positive)
        ((< x 0) 'negative)
        ((= x 0) 'zero)))

In the definition, this triggers these lines.

(defmacro kond (&rest clauses)
  (if (null clauses)
      nil
(let ((clause (first clauses)) ;
(rest (rest clauses))) ;
(if (eq (first clause) 't) `(progn ,@(rest clause))
`(if ,(first clause) ;
(progn ,@(rest clause)) ;
(kond ,@rest)))))) ;

The following forms have the corresponding values.

  • clauses is '(((> x 0) 'positive) ((< x 0) 'negative) ((= x 0) 'zero))
  • clause is (first clauses) which is ((> x 0) 'positive)
  • rest is (rest clauses) which is '(((< x 0) 'negative) ((= x 0) 'zero))
  • (first clause) is (> x 0)
  • (rest clause) is '(positive)

So now we can put them all together into this template

`(if ,(first clause)
     (progn ,@(rest clause))
     (kond ,@rest))

to get

(if (> x 0)
    'positive
    (kond ((< x 0) 'negative)
          ((= x 0) 'zero)))

We see that there is a recursive call to kond so we continue transforming that call. It will lead to two more recursive calls to kond finally resulting in:

(if (> x 0)
    (progn 'positive)
    (if (< x 0)
        (progn 'negative)
        (if (= x 0)
            (progn 'zero)
            nil)))

And this, of course, is precisely what we want kond to do. The transformations specified for the kond macro are all carried out before any computation is done. Only when we have the fully expanded form, does the computation begin.

Using SLIME in Emacs, you can see these expansions in action. Put the cursor on the name of the macro (kond) at a call location and press C-c M-e to enter macrostep-mode. Now every time you press e it will expand further. Press q when you are done to exit macrostep-mode.

Macros

Macro evaluation is a two-step process

  1. An s-expression is built, usually by filling in a template written with backquote (`)
  2. The s-expression is evaluated.

Lisp already has a one-legged if called when. It works like this.

(bind ((x 4))
  (when (> x 0)
    'positive))

We can see that it expands into if using macrostep-mode

(bind ((x 4))
  (IF (> X 0)
      'POSITIVE))

So the only reason for the existence of when is to indicate that we do indeed want the conditional to be one-legged.

Let's write a macro for our own version of when which we will call whenever. It looks like this.

(defmacro whenever (test form)
  `(if ,test ,form))

We see that it is a template which drops the test and form into an if.

What happens when we make the following call?

(bind ((x 4))
  (whenever (> x 0)
    'positive))
  1. The Lisp interpreter sees that whatever is a macro and gets its template `(if ,test ,form)
  2. It drops test and form into the template as specified without evaluating them (if (> x 0) 'positive). This is called macro expansion.
  3. It evaluates the resulting form.

Sometimes further expansion is required when the macro expansion yields a form which itself contains macros, such as with kond. This expansion continues until a form is reached which no longer contains any macro call. That resulting form is then evaluated.

Author: Breanndán Ó Nualláin <o@uva.nl>

Date: 2026-04-15 Wed 15:53