I. Macros
If you've read any gothic horror literature, you may have come across
an author by the name of H.P. Lovecraft. It's odd stuff, to be sure,
and Lovecraft has a penchant for begging the question of describing
just how horrible the horrors are in his stories by resorting to
things like "it was the horror that could not be named" or
"the thing that could not be described" and so on. It gets a little
tedious after awhile.
How does that relate to 1321X? It's simple...every so often some
issue will come up and I'll say something like "well, that's not
really a function, and it doesn't evaluate its arguments when
it's called, and how to write something like that is BEYOND
THE SCOPE OF THIS COURSE." It struck me that my answer was sort
of Lovecraftian in its own way, so I've decided to put how to
write something like that within the scope of this course.
You'll thank me for it later. Sure.
We told you way back when that in Scheme, procedures and data were for
all intents and purposes the same thing, and that there were ways
in which procedures could create other procedures. One of the mechanisms
available to you for creating procedures at run time is something called
the "macro". A macro works like a function, except that when Scheme 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. It's nothing more than a text
substitution for the most part. 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.
II. 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 Scheme 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:
(define (my-if test then-part else-part)
(cond (test then-part)
(else else-part)))
That looks reasonable, no? Let's test it:
> (my-if #t 2 3)
2
> (my-if #f 2 3)
3
Perfect. Exactly what we want. Let's test it just a little bit more:
> (my-if #t (display "yes") (display "no"))
yesno
OOPS!! What happened here? Why does "my-if" do that? The problem is that
"my-if" evaluates its arguments when called, 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. 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 Scheme compiler. You can't write a special form; they're
written only by the folks who created the compiler. But, "if" could be
implemented as a macro, and that's something you could do yourself. How? By
using another special form called "define-macro":
(define-macro my-if
(lambda (test then-part else-part)
(list 'cond (list test then-part)
(list 'else else-part))))
When Scheme then sees:
> (my-if #t (display "yes") (display "no"))
it first "expands the macro", replacing the macro call with:
(cond (#t (display "yes"))
(else (display "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.
Next we'll look at a different syntax for creating macros that's easier on
the eyes, but first some cautionary notes. The approach to macros that we're
looking at here is not part of the Scheme language standard. The
"define-macro" feature is specific to our Dr. Scheme system, so it's not
very portable to other Scheme systems, and that's generally a bad thing.
To get this stuff to work on your system, you'll need to pull down
Dr. Scheme's "language" menu, click "choose language", and then under
"Professional Languages" you'll click on "PLT" and choose "Pretty Big"
Scheme. I hadn't chosen "Pretty Big" Scheme on my laptop, and that's
why my example didn't work in class today.
On the other hand, it's a fairly simple macro capability, and that makes it
a good thing for you, the relatively new programmer. But in the long run,
keep in mind that macros are useful features, they exist in languages besides
Scheme, and the principles of use are generally the same regardless of which
language, or even which dialect of a given language.
III. Backquote-comma syntax
We could write our macros they way we did 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 Scheme 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:
(define-macro my-if
(lambda (test then-part else-part)
`(cond (,test ,then-part)
(else ,else-part))))
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?
IV. 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.
Let's say you're a side-effect-happy programmer who longs for the old days,
and you'd like to be able to increment some numeric variable x by 1 just by
typing in
(++ x)
instead of
(set! x (+ x 1))
Would that be neat or what? Can it be a function?:
(define (++ x)
(set! x (+ x 1))
Let's test it:
>(define test 3)
>(++ test)
>test
3
No, that won't work, because of lexical scoping. But if we had just written
(set! x (+ x 1)) in place of the function call to (++ test), we'd get what
we want. A macro will allow us to do what we want here. And that would be
an easy macro to write. 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: (++ x)
expansion: (set! x (+ x 1))
Then, we begin the macro definition like this:
(define-macro ++
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:
(define-macro ++
(lambda (var)
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: (++ x)
\ \
\ \
\ \
expansion: (set! x (+ x 1))
Now we look at the expansion, and while reading the expansion, we start
constructing the macro body. We begin with a backquote:
(define-macro ++
(lambda (var)
`
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:
(define-macro ++
(lambda (var)
`(
And then, for each expression in the expansion, we do the following:
1. If there is no line connecting it with the macro call, we write down
the expression itself, as is.
2. 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, "set!", we write it
down as is:
(define-macro ++
(lambda (var)
`(set!
On the other hand, "x" does have a connecting line, so in the macro body we
insert the corresponding parameter, preceded by a comma:
(define-macro ++
(lambda (var)
`(set! ,var
And we do this stuff until we're done:
(define-macro ++
(lambda (var)
`(set! ,var (+ ,var 1))))
That was easy, wasn't it?
V. Advantages and disadvantages of macros
There are two key advantages to using macros:
1. 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 Scheme, especially languages that aren't
constrained in ways that functional programming languages often are.
2. Macros produce faster (but more) code than functions because they avoid
the overhead of function calls.
But there are also disadvantages:
1. If you compile macros, you can't detect them with your usual debugging
tools. Things like the macro name goes away once the text replacement
is done.
2. Since macros aren't functions, they can't be used with things that
expect functions, like "apply".
3. Again, if you compile macros, when you redefine a macro, you must
recompile every function whose source 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. It's not
a big deal for you to worry about now, but it's something that might
bite you in the years to come if you're working with macros.
4. Macros produce more (but faster) code than functions because additional
code is added to your program every time a macro call is expanded.
There are lots of other useful things to know about macros, issues like
"variable capture", but those sorts of things are, um, beyond the scope
of this course.
Copyright (c) 2003 by Kurt Eiselt. All rights reserved, with
the exception of stuff that belongs to somebody else.
Last revised: November 13, 2003