Macro programming allows you to mess around inside your Lisp compiler/interpreter to turn Lisp into your own custom programming language. When faced with a difficult programming challenge, many experienced Lispers will first ask themselves, “What programming language could I use to make this problem easy to solve?” Then they’ll use macros to convert Lisp into that language!
No other programming language possesses such a simple and comprehensive macro system. One can even argue that it would be impossible to add this feature to other programming languages, for a simple reason: The Lisp languages are the only ones in which computer code and program data are made out of the same “stuff.” As discussed many times in this book, the fundamental structures for storing data in Lisp are symbols, numbers, and lists, which are made of cons cells. Similarly, the code of a Lisp program is made out of these same basic building blocks. As you’ll see in this chapter, this symmetry between code and data in Lisp is the magic that makes the Lisp macro system possible.
You may have heard that other programming languages, such as C++, also have a feature called macros. For instance, in the C++ language, you would create these using the #define directive. However, these are not the same thing! Lisp macros work in an entirely different and far more sophisticated way.
Sometimes when you’re writing a computer program, you get a feeling of déjà vu. I’m sure you know this feeling. You’re typing away at your computer, and you suddenly realize, “Hey, this is the third time this week I’ve written this same fragment of code!”
Suppose, for example, that your program needs a special add
function:
(defun add (a b) (let ((x (+ a b))) (format t "The sum is ˜a" x) x))
This function adds together two numbers and prints out the sum on the REPL as a side effect. You might find this function useful in a program during debugging:
> (add 2 3)
The sum is 5
5
This add
function seems straightforward, but its code has an annoyance: Why do you need so many parentheses to declare your variable x
? The let
command requires so many parentheses that when you need only a single variable, the code ends up looking especially ludicrous.
The parentheses required by let
are an example of the kind of visual noise a programmer must deal with almost every day. However, you can’t just write a regular function to hide those parentheses, because the let
command can do things a regular Lisp function can’t support. The let
command is a special form. It’s a core part of the language and has special powers beyond those of a standard Lisp function.
Macros let us get rid of the superfluous parentheses. Let’s create a new macro named let1
:
(defmacro let1 (var val &body body) `(let ((,var ,val)) ,@body))
As you can see, the definition of a macro looks similar to the definition of a function. However, instead of using defun
, we use defmacro
to define it. Like a function, it has a name (in this case, let1
) and arguments passed to it .
Once we’ve defined the macro let1
, it can be used just like let
, except that it works with fewer parentheses:
>(let ((foo (+ 2 3)))
(* foo foo))
25 >(let1 foo (+ 2 3)
(* foo foo))
25
Although a macro definition looks very similar to a function definition, a macro is actually very different from a function. To understand why, imagine your Lisp is actually a cute little blob, merrily running your Lisp programs.
This blob understands only standard Lisp code. If it were to see our let1
command, it would have no idea what to do.
Now imagine that we have a magic wand that transforms the appearance of our code just before Lisp gets a peek at it. In our example, it will transform let1
into a regular let
, so Lisp will stay happy.
This magic wand is called macro expansion. This is a special transformation that your code is put through before the core of the Lisp interpreter/compiler gets to see it. The job of the macro expander is to find any macros in your code (such as our let1 macro) and to convert them into regular Lisp code.
This means a macro is run at a different time than a function is run. A regular Lisp function runs when you execute a program that contains the function. This is called runtime. A macro, on the other hand, runs before the program does, when the program is read and compiled by your Lisp environment. This is called macro expansion time.
Now that we’ve discussed the basic thinking behind Lisp macros, let’s take a closer look at how let1
was defined.
When we define a new macro with the defmacro
command, we’re basically teaching the Lisp macro expansion system a new transformation that it can use to translate code before running a program. The macro receives raw source code in its arguments, in the form of Lisp expressions. Its job is to help the macro expander transform this raw code into standard Lisp code that keeps the Lisp blob happy.
Let’s take a closer look at how our let1
macro gets transformed. Here is its definition once again:
(defmacro let1 (var val &body body) `(let ((,var ,val)) ,@body))
The first line of this defmacro
call tells the macro expander, “Hey, if you see a form in code that begins with let1, here’s what you need to do to transform it into standard Lisp.” A macro defined with defmacro
may also have arguments passed into it, which will contain the raw source code found inside the macro when the macro is used. The let1
macro has three such arguments passed into it: var
, val
, and body
. So what do these three arguments represent?
As you can see, when we use let1
, we’ll end up having three different expressions inside it, which are the arguments to the let1
macro:
var
The first argument is the name of the variable we’re defining. This name will be available within our macro using the argument named var
. In this example, it will equal the symbol foo
.
val
The second expression holds the code that determines the value of the variable. In our macro, this is the second argument, val
. It will equal the list (+ 2 3)
.
body
The third expression inside a let1
call is the body code, which makes use of the new variable that’s created (in this case, foo
). It will be available in the macro through the argument named body
.
Since the let
command is allowed to have multiple statements in its body, we will want to mirror this behavior in the let1
macro. This is why, in the defmacro
command defining let1, the final body
argument has the special keyword &body
in front of it. This tells the macro expander “Give me all remaining expressions in the macro in a list.” Because of this, the body argument in our let1
example is actually ((* foo foo))
—a nested list. In this example, we put only a single statement inside let1
.
Now that you’ve seen what the values to the arguments of our let1
macro are, let’s see how the macro uses this information to transform the let1
into a standard let
that the Lisp compiler can understand. The easiest way to transform source code in Lisp is to use backquote syntax. (If you don’t remember how to use backquotes, please see How Quasiquoting Works in How Quasiquoting Works.) With backquotes, we can build the code for a proper let
command using code passed to let1
. Here’s our let1
macro again for reference:
(defmacro let1 (var val &body body) `(let ((,var ,val)) ,@body))
As you can see, the let1
macro returns a backquoted list starting with the symbol let
, followed by the variable name and value, placed in a proper nested list, which Lisp’s let
command requires. The commas cause the actual variable name and value to be plopped in at these locations. Finally, we place the body
code from the let1
in the analogous place in the let
command .
The body
argument is inserted into the transformed code using the splicing comma (,@)
. To understand why the body
needs to be handled in this special way, consider the following use of our let1
macro:
>(let1 foo (+ 2 3)
(princ "Lisp is awesome!")
(* foo foo))
Lisp is awesome! 25
In this case, we’ve put more than one thing inside the body of our let
. Remember that the let
command includes an implicit progn
command, and it can have multiple Lisp instructions inside. Our new let
1 macro allows for this as well by placing the special &body
marker in front of the body
argument, causing all remaining syntax expressions to be passed into let1
as a list. So, in the preceding example, the body
argument contains the code ((princ "Lisp is awesome!") (* foo foo))
.
Now that we’ve written our let1
macro, let’s rewrite our custom add
function in a cleaner way:
(defun add (a b) (let1 x (+ a b) (format t "The sum is ˜a" x) x))
Isn’t this much easier on the eyes?
We can use the macroexpand
command to see code generated by a macro. Simply pass the macro’s code to macroexpand
, like this:
>(macroexpand '(let1 foo (+ 2 3)
(* foo foo)))
(LET ((FOO (+ 2 3))) (* FOO FOO)) ; T
You can now see the raw code generated by let1
. The T
at the end just means macroexpand
was handed a valid macro that it was able to expand.
As your macros become more complex, you’ll find that macroexpand
is a valuable tool in testing and debugging their structure.
3.14.131.47