10

MACROS

Macros are a controversial feature of Lisp. On the one hand, they are one of the main reasons why Lisp is so powerful and flexible. They are a defining feature, enabling a masterful macro writer to bend the language to his will. There are no limits; you can rewrite the language in your own image.

It's such a compelling feature that I use it to advertise this book.

On the other hand, many experienced users of Lisp warn against the use of macros. While some Lisp advocates strongly encourage the use of macros, Peter Norvig in Paradigms of Artificial Intelligence Programming writes:

The first step in writing a macro is to recognize that every time you write one, you are defining a new language …. Introducing a macro puts much more memory strain on the reader of your program than does introducing a function, variable or data type, so it should not be taken lightly. Introduce macros only when there is a clear need, and when the macro fits in well with your existing system.

In other words, macros are powerful and require experience to use properly. In fact, they require experience just to understand their value and power. Many Lisp-skeptics question whether their benefits are worth the cost of 1. being forced to learn many different DSLs; and 2. using a niche language like Lisp over languages with more energetic ecosystems.

Lisp advocates find it challenging to convince skeptics to give Lisp a serious look because demonstrating the value of macros for common programming tasks is honestly not possible, and it's easy to imagine the nightmare of domain specific languages all over the place written by people unqualified to write more than a CRUD app. Macros are something that need to be experienced to be understood–similar to Lisp's image-based programming model. Unfortunately, "You have to try it to understand" sounds a lot like cope from delusional nerds advocating for solutions without recognizing the weaknesses of their solutions (something nerds are wont to do).

However, Lisp-curious programmers are often attracted to the platonic ideal of macros. Its the fruit when eaten that grants forbidden knowledge that they hope will give them an edge over the programmers using lesser languages. When faced with a problem, macros are the first thing that these people (maybe even you, my smart and handsome reader) want to try to use to solve the problem. The warnings of the graybeards make macros ever more tantalizing, and the macro-skeptics create an opponent in the mind of the macro-maximalist who sees them as the losers they will crush when they finally unlock the true potential of meta programming.

If you're reading this, you are likely among those curious to learn more about macros in particular. Because macros are such a deep subject, even if I were a macro wizard (which I am not), I couldn't provide a tutorial expansive and worthy enough to be called complete. Here in this chapter I can only teach the essential things to know about macros. See the RESOURCES chapter to get a list of resources for learning more about macros and more.

First, I think it's useful to see what macros can do.

The almighty-html </> macro

This is the </> macro from the almighty-html library. It produces HTML.

(ql:quickload "almighty-html")
(in-package :almighty-html)
(</>
 (div :id "card-container"
   (a :href "/path/to/book-1"
     (div :class "card"
       (h2 :class "card-title" "Book 1")
       (p :class "card-body" "This is book 1 of the series of books.")))))
;  => 
Result<div id="card-container">
  <a href="/path/to/book-1">
    <div class="card">
      <h2 class="card-title">Book 1</h2>
      <p class="card-body">This is book 1 of the series of books.</p>
    </div>
  </a>
</div>

The sxql macro

This is a SQL query produced with the sxql library using several macros.

(ql:quickload "sxql")
(in-package :sxql)
(yield (select :*
         (from :users)
         (where (:= :age 30))))
Result"SELECT * FROM users WHERE (age = ?)", (30)

The Coalton language

This is example code for Coalton (https://www.youtube.com/watch?v=of92m4XNgrM), a new programming language built on top of Common Lisp. It's "just a macro".

(declare create-account (AccountName -> Balance -> BankM (BankResult Account)))
(define (create-account name initial-balance)
  "Adds an account to the BankState and return the created account."
  (run-resultT
   (do
    (accounts <- (lift (lift get)))
    (ResultT
     (match (get-account name accounts)
       ((Err _) (pure (Ok Unit)))
       ((Ok _) (pure (Err (AccountAlreadyExists name))))))
     (let unvalidated-account = (Account name initial-balance))
     (account <- (ResultT (check-account-is-valid unvalidated-account)))
     (ResultT (set-account account)))))

Compilation Before Evaluation

What we see is that macros are code that is capable of either generating, or fully implementing, entirely other languages. Of course, they can produce Lisp code, as the defun and loop macros do.

The most important thing to know about macros is that they are compiled or expanded before they are evaluated. Because of this expansion step, macros are the only tool programmers can use to modify the evaluation of the code it expands into.

In Lisp, the first symbol in a list is evaluated as a function name, and the rest of the values are evaluated from left to right. Before the function name is looked up, the parameters are evaluated.

(hello my name is "Micah")

The error you get here is about a non-existent my variable--hello will be evaluated after the arguments, and my is the first argument.

But what about defun? The whole point is that the first argument is suppose to be bound to the definition that follows.

(defun hello (name)
  (print name))

hello isn't evaluated as a symbol containing a value–it's used as the name for the code that follows.

And that's how we know defun is a macro. In fact, anything called "special operators" are actually just macros.

We can even view what the defun macro expands to right in Emacs. With the cursor over the part that says defun hello, type SPC m m (macrostep-expand). If you are using SBCL as I suggested at the beginning of the book, you should see something like this:

(progn
 (eval-when (:compile-toplevel) (sb-c:%compiler-defun 'hello t nil nil))
 (sb-impl::%defun 'hello
                  (sb-int:named-lambda hello
                      (name)
                    (block hello (print name)))))

Because macros can expand into more macros, you may be able to expand more macros after the first expansion.

(defun hello (names)
  (loop :for name :in names
        :do (format t "Hello, ~a.~&" name)))
;; expands into
(progn
 (eval-when (:compile-toplevel) (sb-c:%compiler-defun 'hello t nil nil))
 (sb-impl::%defun 'hello
                  (sb-int:named-lambda hello
                      (names)
                    (block hello
                      (loop :for name :in names
                            :do (format t "Hello, ~a.~&" name))))))
;; which further expands into...
(progn
 (eval-when (:compile-toplevel) (sb-c:%compiler-defun 'hello t nil nil))
 (sb-impl::%defun 'hello
                  (sb-int:named-lambda hello
                      (names)
                    (block hello
                      (block nil
                        (let ((name nil)
                              (#:l548
                               (sb-kernel:the* (list :use-annotations t
 :source-form names) names)))
                          (declare (ignorable #:l548)
                                   (ignorable name))
                          (tagbody
                           sb-loop::next-loop
                            (when (endp #:l548) (go sb-loop::end-loop))
                            (sb-loop::loop-desetq name (car #:l548))
                            (sb-loop::loop-desetq #:l548 (cdr #:l548))
                            (format t "Hello, ~a.~&" name)
                            (go sb-loop::next-loop)
                           sb-loop::end-loop)))))))
;; and further...
(progn
 (eval-when (:compile-toplevel) (sb-c:%compiler-defun 'hello t nil nil))
 (sb-impl::%defun 'hello
                  (sb-int:named-lambda hello
                      (names)
                    (block hello
                      (block nil
                        (let ((name nil)
                              (#:l551
                               (sb-kernel:the* (list :use-annotations t
 :source-form names) names)))
                          (declare (ignorable #:l551)
                                   (ignorable name))
                          (tagbody
                           sb-loop::next-loop
                            (if (endp #:l551)
                                (go sb-loop::end-loop))
                            (setq name (car #:l551))
                            (setq #:l551 (cdr #:l551))
                            (format t "Hello, ~a.~&" name)
                            (go sb-loop::next-loop)
                           sb-loop::end-loop)))))))

After calling macroexpand-step the first time, you can expand one more step with e.

You can undo one expansion at a time with u or completely revert all expansions everywhere with q.

Defining Macros

As you can see, macros significantly reduce the amount of visual noise in the code and make it easier to understand. But after looking at the code it expands into, you might wonder how the heck you even get started writing a macro at all?

To begin defining a macro, first you need to do some wishful thinking.

  • Write the desired call.
  • Write the desired expansion.
  • Construct macro parameter list from call.

For example, let's say you want to define a when-let macro that combines let and when into one form. First, you begin with the macro call you want to have:

(when-let (user (get-user id))
  (welcome-message user)
  (redirect-to-dashboard user))

Then, you write the code you want it to expand into:

(let ((user (get-user id)))
  (when user
    (welcome-message user)
    (redirect-to-dashboard user)))

Finally, you write the macro definition.

(defmacro when-let ((var expression) &body body)
  `(let ((,var ,expression))
     (when ,var
       ,@body)))

Here you see a backquote in the form `(let ...). Backquotes are like normal single-quotes–they are an abbreviation of quote, which returns the literal representation of some code.

(quote (+ 2 2))
Result(+ 2 2)
'(+ 2 2)
Result(+ 2 2)
`(+ 2 2)
Result(+ 2 2)

Notice that those symbols aren't interpreted. They and the list they are included in are returned as-is.

Backquotes have an important feature: you can tell the interpreter to actually evaluate some of parts of the code.

To tell the interpreter to evaluate parts inside a backquoted form, type , before the part you want evaluated.

(let ((a 2)
      (b 3))
  `((+ ,a ,b) is ,(+ a b)))
Result((+ 2 3) IS 5)

There are some rules you have to follow when using backquotes:

  • A comma must follow a backquote.
  • A comma can't follow another comma.

For example:

(let ((a 2)
      (b 3))
  `((+ ,a ,b) is ,(+ ,a ,b)))
ResultComma not inside a backquote.
  Stream: #<dynamic-extent STRING-INPUT-STREAM (unavailable) from "(let ((*...">
   [Condition of type SB-INT:SIMPLE-READER-ERROR]

If we add the commas to the second a and b, since the outer (+ ...) form is already preceded with a comma, we get that error.

In the when-let macro definition, we also saw ,@body. The ,@ "splices" the form into the backquoted code–removing the outermost parentheses.

(let ((a 5)
      (b '(1 2 3))
      (c 9))
  `(+ ,a ,@b ,c))
Result(+ 5 1 2 3 9)

,@ is commonly used with macros that take an arbitrary number of arguments and passing those arguments along to another form that also takes an arbitrary number of arguments. That's what the body parameter for when-let is.

After having the call and expansion prepared, you need to look at the parts that are common between them, and then make those parameters in the defmacro lambda-list.

For example, look at user. It's in the expansion code three times:

(let ((user ...))
 (... user
   (... user))

and in the macro call twice:

(when-let (user ...)
  (... user))

Which means a parameter in the when-let definition needs to correspond to this variable.

(defmacro when-let ((var ...)...)
  ...)

…and any instance of var in the body of the macro definition needs to be preceded with a comma.

(defmacro when-let ((var ...)...)
  `(let ((,var ...))
     (when ,var
       ...)))

The same goes for (get-user id). It appears once in the expansion and once in the macro call.

(let ((... (get-user id)))
 (... ...
   (... ...)))

(when-let (... (get-user id))
  ...)

So it gets a spot in the parameter list of the macro:

(defmacro when-let ((... expression) ...)
  ...)

And where expression appears in the macro body, it's preceded with a comma.

(defmacro when-let ((... expression) ...)
  `(let ((... ,expression))
     ...))

The calls to (welcome-message user) and (redirect-to-dashboard user) are actually in a space where we expect any arbitrary number of arbitrary forms. For that, we use a &body parameter and splice it in with ,@.

(defmacro when-let ((... ...) &body body)
  `(let ((... ...))
     (when ...
       ,@body)))

Altogether:

(defmacro when-let ((var expression) &body body)
  `(let ((,var ,expression))
     (when ,var
       ,@body)))

Destructuring Arbitrary List Structures In Parameters

In the example of when-let, we saw something strange in the lambda-list. There's a nested list (var expression):

(defmacro when-let ((var expression) &body body)
  ...)

What's going on there?

Macros can take any list structure as a parameter and destructure them–binding parameters to elements within the list passed to the macro.

To understand this characteristic of macro parameters, first we need to understand destructuring, represented best by destructuring-bind.

destructuring-bind takes an arbitrary list structure and binds the elements of the list to local variables.

(let ((people '(("Micah" 40 ("Lisp" "Hiking" "Photography"))
                ("Guy" 71 ("Engineering" "Frogs" "Education"))
                ("Joe" 50 ("Games" "Civilization" "Finding Good Women")))))
  (dolist (person people)
    (destructuring-bind (name age (interest1 interest2 interest3)) person
      (print interest1))))
Result"Lisp" "Engineering" "Games" => NIL

The first argument to destructuring-bind is a lambda-list of the variables to bind. The key is that it can bind any arbitrary element in any arbitrary list structure directly. All you need to do is mimic the structure of the list in the parameter list to destructuring-bind.

(let ((tree '(1 (2 (3 4) 5 ((6 ((7))))))))
  (destructuring-bind (a (b (c d) e ((f ((g)))))) tree
    (format t "~a + ~a = ~a" d f (+ d f))))
Result4 + 6 = 10 => NIL

Macros can similarly destructure lists in their parameters list:

(defmacro add-elements ((a (b (c))))
  (+ a b c))

Calling it:

(add-elements (1 (2 (3))))
Result6

It's important to remember that we didn't call (add-elements '(1 (2 (3))) or (add-elements (list 1 (2 (3))): those are both single elements. With a function, this would be an invalid call because the 3 (the innermost element) would be interpreted as a function-name, the Lisp evaluator would look for it, and not find it.

However, macros have the previous macro expansion/compilation step, so the form (1 (2 (3))) is valid: the macro would be expanded before the elements are evaluated.

Redefining Macros

One thing to keep in mind with macros is that if you make a macro, use the macro to write some code and compile that code, if you redefine the macro, you also need to recompile the other code. Otherwise, that code is using the old macro expansion and not the new one.

Determining When To Use Macros

As I said before, the general rule is to be conservative with your creation of macros. But there are times when you just gotta use the facilities that a macro provides.

Transformation

Macros allow you to expand into different forms based on the arguments you send to it--before the arguments are evaluated. Consider the macro setf.

(defvar *lang*)
(defstruct vehicle
  make
  model
  color)
(defclass person ()
  ((name :initarg :name :initform nil :accessor person-name)
   (age :initarg :age :initform nil :accessor person-age)))
(defparameter *cruiser* (make-vehicle :make "Toyota" :model "Land Cruiser 70"
 :color "Beige"))
(defparameter *micah* (make-instance 'person :name "Micah" :age 40))
(defparameter *list* '(1 2 3))

(setf *lang* "Japanese")
(setf (car *list*) 11)
(setf (vehicle-color *cruiser*) "White")
(setf (person-age *micah*) 25)

The calls to setf expand to:

(setq *lang* "Japanese")
(sb-kernel:%rplaca *list* 11)
(let* ((#:*cruiser*575 *cruiser*) (#:new574 "White"))
  (funcall #'(setf vehicle-color) #:new574 #:*cruiser*575))
(let* ((#:*micah*579 *micah*) (#:new578 25))
  (funcall #'(setf person-age) #:new578 #:*micah*579))

The setf macro looks at the arguments it receives and decides how to expand based on what it gets. If it sees (car ...) then it uses rplaca, if it sees another function like (person-age ...) then it calls a generic function setf with the PERSON-AGE setter sent to it.

It does this without evaluating the expressions passed to it–it only knows the literal representations of the forms. The code is the data it works with. If you need to transform code based on the shape of its inputs, you might need a macro.

Binding

As I said before, symbols aren't evaluated at expansion/compilation time, so if you want to bind symbols to values, macros are probably what you need. That's why defun, etc. are macros.

Conditional Evaluation

Because macros can transform code and control the evaluation of code, when you need to control the order of evaluation or whether a form gets evaluated at all, you might need a macro. That's why if, when, unless, etc. conditional forms are macros.

Wrapping An Environment

When you want to create a lexical environment like the with- family of macros do, then you probably want a macro.

Determining When Not To Use Macros

There are certain situations under which you absolutely can't use macros. Macros can't be used as data in the same way functions can.

(mapcar #'my-macro data)

Only functions can be passed to higher-order functions like mapcar, reduce, remove-if-not, etc.

And of course, always keep in mind that if you are trying to develop a DSL, you might be overcomplicating things. If you want to collaborate with others, and you want to write DSLs, you better make them good.

Variable Capture & Hygiene

Macros can be tough to debug. That's partially because macros are generally harder to read because you have to keep the expansion code and expander code in your head at the same time.

But macros are also more difficult to debug because they are subject to a unique class of bugs called variable capture. The subject of macro hygiene is primarily concerned with avoiding this class of bugs.

The simplest example of a macro that's vulnerable to variable capture is a macro that returns a free variable.

(defmacro might-get-captured1 ()
  '(+ x 1))

Why is this macro vulnerable? Because it doesn't return '(+ x 1). It expands into (+ x 1), which is then evaluated at runtime.

(might-get-captured1)
ResultThe variable X is unbound.
   [Condition of type UNBOUND-VARIABLE]
(let ((x 1))
  (might-get-captured1))
Result2

Try expanding the code with SPC m m (macrostep-expand) to verify.

Compare to a function:

(defun cant-capture-me ()
  '(+ x 1))

Calling it:

(cant-capture-me)
Result(+ X 1)
(let ((x 100))
  (cant-capture-me))
Result(+ X 1)

So it's important to remember that quotes/backquotes in functions return literal representations, whereas macros use quotes/backquotes as templates for expanding into code-to-be-evaluated.

In addition to macros with free variables, macros that bind multiple variables within one let are also vulnerable to capture. Consider the following macro:

(defmacro might-get-captured2 (variable)
  `(let ((x 1)
         (,variable 0))
     (values x ,variable)))

If we call it like this:

(let ((y 5))
  (might-get-captured2 y))

What do you expect the return values to be?

1, 0

Expanded:

(let ((y 5))
  (let ((x 1) (y 0))
    (values x y)))

Or what about this wombo combo:

(let ((x 5))
  (might-get-captured2 x))
ResultExecution of a form compiled with errors.
Form:
  (LET ((X 1) (X 0))
  (VALUES X X))
Compile-time error:
  The variable X occurs more than once in the LET.
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]

It returns an error because x occurs twice in the expanded let form.

(let ((x 5))
  (let ((x 1) (x 0))
    (values x x)))

You don't know what symbols could be in a &body parameter of a macro. A symbol used in the macro may get used in the &body as well.

(defmacro might-get-captured3 (&body body)
  `(let ((x 5))
     (+ x ,@body)))

Because the body expands into an environment where x is bound to the value 5, if the symbol x appears in the body, it will likely be overwritten by the macro's let context.

(let ((x 70)
      (y 50))
  (might-get-captured3
    (+ x y)))
Result60

So how do you avoid variable capture?

Better Names For Global Variables

Make sure you name global variables using the *...* convention.

Assign Parameters To Local Variables

Instead of using parameters directly…

;; Vulnerable
(defmacro comes-before-p (x y sequence)
  `(let ((sequence ,sequence))
     (< (position ,x sequence)
        (position ,y sequence))))

Calling it:

(comes-before-p 1 5 '(1 2 3 4 5))
ResultT

…assign parameters to local variables before using them.

(defmacro comes-before-p (x y sequence)
  `(let ((sequence ,sequence)
         (xvar ,x)                         ; different variable names
         (yvar ,y))                        ; different variable names
     (< (position xvar sequence)
        (position yvar sequence))))

Define Macros In Separate Packages

To some extent, simply following the more modern One-Package-Per-File convention for factoring projects (covered in the next chapter) can also prevent some instances of variable capture. If you have my-macro with symbol x inside it, if someone uses my-macro in their own package with an x symbol, they will actually be two different symbols: my-macro-package::x and their-package::x.

This is also true of macros within your own systems with multiple packages. my-package/a:x and my-package/b:x are two separate symbols in two separate packages. Even if the macro my-package/a:my-macro uses x and you use that macro in my-package/b which also has my-package/b:my-other-macro that also uses the symbol x, they won't capture each other because they are naturally namespaced inside their own packages. This will make more sense once we learn more about packages and systems.

Use gensym

Even if you give variables different names, and even if you give them names that others are unlikely to use, you have no guarantees that they will never be used by users of your macro.

Instead, you can create anonymous symbols with gensym.

(gensym)
Result#:G586

Every time you call gensym, it will generate a symbol with a unique name.

Above, we expanded several different calls to setf to show how it transforms based on the data it's passed. When we passed it (vehicle-color *cruiser*), it expanded into this:

(let* ((#:*cruiser*575 *cruiser*) (#:new574 "White"))
  (funcall #'(setf vehicle-color) #:new574 #:*cruiser*575))

You can see the variables generated by gensym: #:*cruiser*575 and #:new574. By default, symbols generated with gensym are prefixed with G, but we can change that:

(gensym "*CRUISER*")
Result#:*CRUISER*592

We can even change the suffix number (usually derived from *gensym-counter*) if we pass an integer.

(gensym 500)
Result#:G500

Importantly, these anonymous symbols aren't interned, so you can't even find them if you go searching.

(find-symbol "*CRUISER*592" )
ResultNIL, NIL

And in fact, even if they appear to have the same name, two symbols generated with gensyms are still entirely different objects in memory.

(let ((a (gensym 100))
      (b (gensym 100)))
  (values a b))
ResultG100, G100
(let ((a (gensym 100))
      (b (gensym 100)))
  (eq a b))
ResultNIL

Once you find a variable that can possibly be captured, saving it to an anonymous symbol is a bulletproof way to avoid capture.

(defmacro comes-before-p (x y sequence)
  (let ((xvar (gensym))
        (yvar (gensym)))
    `(let ((sequence ,sequence)
           (,xvar ,x) 
           (,yvar ,y))
       (< (position ,xvar sequence)
          (position ,yvar sequence)))))

Much More

Like I said, there is much more to say about macros, but this is not the book for and I'm not the guy to talk about macros. Refer to the RESOURCES chapter to find out where to learn more about macros.