Defmacro
2010-12-11
Introduction
defmacro
macros are not actually part of
Scheme (they are from Common Lisp). However, many Scheme implementations
support them, and they are conceptually simple. I will explain
defmacro
macros first, before moving on to Scheme's
syntax-rules
macros.
Examples
A defmacro
macro is defined as follows:
(defmacro name (arguments)
body)
This defines the macro name, which takes the specified arguments. The argument list is just like for lambda. To expand the macro, body is evaluated, and the return value is substituted in place of the original invocation of the macro. A few examples:
; Define a macro that returns the code it is passed
(defmacro id (x) x)
; The following expands to (newline)
(id (newline))
; Define a macro that will cause the argument to be
; evaluated twice
(defmacro twice (x) `(begin ,x ,x))
; Prints four newlines
(twice (twice (newline)))
On a very basic level, that's it. You just do whatever you want with the arguments and return some list. The list will then be processed as if it had been there instead of the original macro invocation. Macros can expand to other macro invocations, and you can make it all as complex as you wish.
pred
Now for a more useful example. Suppose you find yourself writing a lot of code that matches a value against a number of predicates:
(cond
((number? x) ...)
((symbol? x) ...)
((string? x) ...)
((list? x) ...))
It would be nice if Scheme allowed youto write:
(pred x
(number? ...)
(symbol? ...)
(string? ...)
(list? ...))
Now, Scheme doesn't have a pred
construct.
But you can write one! The following macro transforms the
pred
code above into the cond
code before
it:
(defmacro pred (expr . alts)
`(cond
,@(map
(lambda (x)
(cons (list (car x) expr)
(cdr x)))
alts)))
Note how the macro processes each alternative by extracting the predicate being matched, and replacing it by a function call to the predicate, with the expression as an argument. Congratulations, you have just extended Scheme with another conditional form! However, there is a problem. Read the following code, think about what you would expect it to do, and how it would be expanded:
(pred (read)
(symbol? 'symbol)
(number? 'number)
(string? 'string))
The intention seems to be to read one expression, and
return symbol
if it's a symbol, number
if it's
a number, etc. However, from the expansion, it's clear that this is not
the actual behavior:
(cond
((symbol? (read)) 'symbol)
((nuber? (read)) 'number)
((string? (read)) 'string))
read
is called up to three times! To bring
the behavior in line with expectations, we can use let
to
first bind the value returned by (read)
to a variable, and
then pass that variable to pred
. Better yet, we can include
this in the macro:
(defmacro pred (expr . alts)
`(let ((-pred-x- ,expr))
(cond
,@(map (lambda (x)
(cons (list (car x) '-pred-x-)
(cdr x)))
alts))))
This fixes one problem, but introduces another, much
subtler problem, called variable capture. To understand what's
happening, consider what would happen if the symbol
-pred-x-
is actually used in one of the alternatives. For
example:
(let ((-pred-x- 12))
(pred 'foo
(symbol? (write -pred-x-) (newline))))
You may expect the code above to write 12
,
but it actually writes foo
! This becomes obvious when you
look at the expansion:
(let ((-pred-x- 12))
(let ((-pred-x- 'foo))
(cond
((symbol? -pred-x-)
(write -pred-x-)
(newline)))))
The inner -pred-x-
shadows the outer one,
and, as a result, write
is passed the value
foo
. Common Lisp, and probably any Scheme
implementation that provides defmacro
, includes a
function gensym
that generates a symbol that is
guaranteed to be unique. By using such a symbol for the macro
expansion, we can make sure that variable capture does not occur. A
typical macro using gensym
looks like this:
(defmacro my-macro (...)
(let ((sym (gensym)))
`(let ((,sym ...))
...)))
Note how we first bind the name of our symbol using
let
, but this let
is not actually part of the
expansion of the macro. This ensures that variable capture will not
occur. However, the readability of the macro certainly has not
improved.
There are a few variations on the defmacro
syntax used
here. The original defmacro
system in Common Lisp uses
&rest
to signal that the next symbol should be bound to the
rest of the argument list, rather than Scheme's .
used
here. TinyScheme uses
an even more Scheme-like syntax (analogous to define
):
(macro (name arguments)
...)
You will have to experiment to see how your Scheme
implementation supports macros, or use syntax-rules
macros,
which are in the Scheme standard.