Chapter 16. The Magic of Lisp Macros

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.

image with no caption

Note

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.

A Simple Lisp Macro

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

Macro Expansion

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.

image with no caption

This blob understands only standard Lisp code. If it were to see our let1 command, it would have no idea what to do.

image with no caption

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.

image with no caption

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.

How Macros Are Transformed

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?

image with no caption

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 let1 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)).

Using the Simple Macro

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.131.47