CS 2360 Lecture 17: February 9
Macros
One of the key components in extending LISP or building new
languages on top of LISP is the "macro". A macro works like
a function, except that when LISP sees a call to a macro in
some code that is being executed, the definition of the macro
is inserted in place of the macro call. This is in contrast
to a "normal" function call which results in a copy of the
current environment being saved on the program stack,
followed by the execution of the function definition, which
in turn is followed by the "popping" of the program stack.
Another way to think of the difference between functions and
macros is this: A function produces results, while a macro
produces expressions which, when evaluated, produce results.
We've seen lots of macros in use already: dolist, dotimes,
setf, do, defun, case, etc. You should look at pp. 1026-27
of Steele for an exhaustive list of predefined LISP macros.
Macros and their arguments
One big important feature of macros is that they allow you to
create constructs which do not evaluate their arguments. The
importance of this may not be immediately obvious. An
example will help you see the light. Say that you had a
version of LISP in which there was no "if" form, and you
wanted to create one. How would you do it?
Your first thought might be to create an ordinary function:
(defun my-if (test then-part else-part)
(cond (test then-part)
(T else-part)))
That looks reasonable, no? Let's test it:
? (my-if t 2 3)
2
? (my-if nil 2 3)
3
?
Perfect. Exactly what we want. Let's test it just a little
bit more:
? (my-if t (print "yes") (print "no"))
"yes"
"no"
"yes"
?
OOPS!! What happened here? Why does "my-if" do that? The
problem is that "my-if" evaluates its arguments, and these
particular arguments have side-effects when evaluated. So,
even before the "my-if" definition is applied to the
arguments, the two "print" expressions are evaluated,
resulting in both "yes" and "no" being printed on the
terminal. Only then is the function applied to the
arguments, and the result of evaluating (print "yes") is
returned. But that's not what "if" does, is it?
How then does "if" work? It's actually something called a
special form, hard-coded into the LISP interpreter. You
can't write a special form; they're written only by the folks
who created the interpreter. But, "if" could be implemented
as a macro, and that's something you could do yourself. How?
By using another macro called "defmacro":
(defmacro my-if (test then-part else-part)
(list 'cond (list test then-part)
(list 'T else-part)))
When LISP then sees:
(my-if nil (print "yes") (print "no"))
it first "expands the macro", replacing the macro call with:
(cond (nil (print "yes"))
(T (print "no")))
In other words, anything that's quoted is used literally, and
anything that's not quoted is replaced by the value it's
bound to at the time the macro call is encountered.
Backquote-comma syntax
We could write our macros as shown above, but the macros that
result don't look much like the code they're going to expand
into. This makes macros hard to read and debug. So a syntax
for macro creation has been invented, which when used makes
these things look more like templates for the code to be
generated.
The backquote-comma syntax introduces two new operators: the
backquote (`) and the comma (,). What did you expect? The
backquote tells LISP to quote everything in the list that
follows (so in that sense it works just like quote), except
for those items immediately preceded by a comma.
The comma works only within the scope of a backquote. It
says "turn off the quoting for now". When it appears in
front of a list element, it cancels out the quote that would
have been in effect otherwise. So instead of the literal
symbol being put into that template, the value of that symbol
at the time of the macroexpansion is put there.
In other words, backquote makes a template while comma makes
a slot in the template.
Here's "my-if" again, using backquote-comma syntax:
(defmacro my-if (test then-part else-part)
`(cond (,test ,then-part)
(T ,else-part)))
Note: In the LISP version of "if", the "else-part" is
actually an optional argument. We could get that same effect
here by doing the following:
(defmacro my-if (test then-part &optional else-part)
:
:
In either case, this expands exactly the same way as our
previous macro definition, but it's much easier to read
because it looks much more like the code that it will turn
into, no?
How to define simple macros (taken from "On LISP" by Paul Graham)
Let's walk through another macro example in gory detail and, along
the way, we'll outline a step-by-step process for defining macros.
You're familiar with "member", right? This function uses "eql" to
test for equality by default. But you could use an optional
argument to use a different test:
(member item my-list :test #'eq)
If we use this a lot, we could make our own variant of "member"
that always uses "eq". In older dialects of LISP, this was called
"memq":
(memq item my-list)
We could define "memq" as an ordinary function:
(defun memq (item my-list)
(member item my-list :test #'eq))
but that wouldn't tell us much about macros, would it? Let's
define "memq" as a macro. Graham tells us that the first step in
macro definition is to begin with a typical call to the macro that
we want to define, and write it on a piece of paper. Below that,
we should write the expression that we want the macro call to
expand into:
call: (memq item my-list)
expansion: (member item my-list :test #'eq)
Then, we begin the macro definition like this:
(defmacro memq
And we construct the argument list for the macro definition from
the parameters in the macro call. To avoid confusion, we invent
new names for each of these arguments:
(defmacro memq (obj lst)
Now we go back to the call and expansion that we wrote down. For
each argument in the macro call, we draw a line connecting it with
the place it appears in the expansion:
call: (memq item my-list)
\ \
\ \
expansion: (member item my-list :test #'eq)
Now we look at the expansion, and while reading the expansion, we
start constructing the macro body. We begin with a backquote:
(defmacro memq (obj lst)
`
Then, whenever we see a parenthesis in the expansion that isn't
part of an argument in the macro call, we put that same
parenthesis in the macro definition:
(defmacro memq (obj lst)
`(
And then, for each expression in the expansion, we do the
following:
- If there is no line connecting it with the macro call,
we write down the expression itself, as is.
- If there is a connection to one of the arguments in the
macro call, we write down the symbol which occurs in the
corresponding position in the macro parameter list,
immediately preceded by a comma (i.e., we don't want this
quoted...instead, we want to pass a value through here).
Since there's no connecting line to the first element, "member",
we write it down as is:
(defmacro memq (obj lst)
`(member
On the other hand, "item" does have a connecting line, so in the
macro body we insert the corresponding parameter, preceded by a
comma:
(defmacro memq (obj lst)
`(member ,obj
And we do this stuff until we're done:
(defmacro memq (obj lst)
`(member ,obj ,lst :test #'eq))
That was easy, wasn't it?
The LISP evaluator, revisited
All this macro stuff suggests that our concept of what the LISP
evaluator does isn't quite complete. Let's update that---here's
how it really works (taken from "Common LISP Programming for
Artificial Intelligence" by Hasemer and Domingue):
- If the expression is an atom, return its value.
- If the expression is a list, then either
- If the first member of the list is a special form,
handle it and its arguments accordingly.
- If the first member of the list is the name of a
macro, call the function "macroexpand" to expand it.
Then go to step 1.
- If the first member of the list is the name of a
function, then retrieve the function body and
- Find the values of the remaining members of the
list by applying these rules to each of them
in turn.
- Apply the function from (2.3) to the arguments
from the rule above and return the result.
Things to remember: First, as noted before, macro expansion does
not evaluate the macro's arguments. The macro is expanded before
the arguments are evaluated by the evaluator. Second, macro
expansion is performed as long as the first element of the
expression is the name of a macro.
The "macroexpand" function
The key to macro expansion in LISP is the "macroexpand" function.
This function takes one argument, which is quoted. And that
argument is a macro call. So let's say we define our "my-if"
macro:
Welcome to Macintosh Common Lisp Version 2.0!
? (defmacro my-if (test then-part else-part)
`(cond (,test ,then-part)
(T ,else-part)))
MY-IF
?
If we then want to see how a macro call to "my-if" will expand, we
can do this:
? (macroexpand '(my-if (eql 1 2) (print "yes") (print "no")))
(IF (EQL 1 2) (PROGN (PRINT "yes")) (COND (T (PRINT "no"))))
T
?
Note that "macroexpand" returns not one result, but two. Weird,
eh? Read pp. 179-187 of Steele for more about this. In any case,
the first value is the expanded code, and the second result tells
us whether the first element of the argument was the name of a
macro. If it was, then the second value returned will be T, and
the first value will be different from the input argument. If the
first element was not the name of a macro, the second value
returned will be nil, and the first value will be the unchanged
input argument.
Also note that the result of the expansion above isn't what we
expected, at least based on our macro definition. That's because
we put a macro call inside our macro definition---"cond" is the
name of a macro. So "macroexpand" did one expansion of the call
to "my-if", and the result of that was an expression beginning
with "cond". Since "cond" is the name of a macro, "macroexpand"
was called again, resulting in yet another expansion. This new
expression began with "if", which is a special form, not a macro,
so macroexpansion ceased at that point.
If we want to see the result of just one level of expansion so we
can check to see if our macro expands the way we intended it to,
we use another function called "macroexpand-1". Here's the result
of one level of expansion:
? (macroexpand-1 '(my-if (eql 1 2) (print "yes") (print "no")))
(COND ((EQL 1 2) (PRINT "yes")) (T (PRINT "no")))
T
Here's the what happens when we expand the result of the previous
expansion:
? (macroexpand-1 '(COND ((EQL 1 2) (PRINT "yes")) (T (PRINT
"no"))))
(IF (EQL 1 2) (PROGN (PRINT "yes")) (COND (T (PRINT "no"))))
T
And here's what happens when we try to expand that result, which
starts with the name of a special form, not a macro:
? (macroexpand-1 '(IF (EQL 1 2) (PROGN (PRINT "yes")) (COND (T
(PRINT "no")))))
(IF (EQL 1 2) (PROGN (PRINT "yes")) (COND (T (PRINT "no"))))
NIL
Advantages and disadvantages of macros
There are two key advantages to using macros:
- The arguments to macros are not evaluated. Thus, new
syntactic constructs can be defined with macros. This
is essential to defining new languages on top of LISP,
especially languages that aren't constrained in ways
that functional programming languages often are.
- Macros produce faster (but more) code than functions
because they avoid the overhead of function calls.
But there are also disadvantages:
- If you compile macros, you can't detect them with your
usual debugging tools, so "trace" and "step" won't work.
- Since macros aren't functions, they can't be used with
"funcall", "apply", or the mapping functions.
- Again, if you compile macros, when you redefine a macro,
you must recompile every function whose sourdce contains
a call to the macro. If you don't, your source code may
contain the results of expanding old macro definitions
instead of the new ones. You may think the new macro has
been expanded where the macro call occurs, but it won't
be. This can be especially aggravating if the old macro
definition resulted in code which didn't blow up. So
your program does something, it just doesn't do what your
new macro definition says it should do. You can spend
lots of hours spinning wheels trying to debug that kind
of thing.
See you at the exam... Friday, first period (ugh) 8 am.
Back To The CS2360 Home Page
Ian Smith (iansmith@cc.gatech.edu)
Last Modified 27 Feb 95 by Ian Smith (iansmith@cc.gatech.edu)