8. MACROS
This chapter contains the following sections:
8.0 Introduction to macros
8.1 Macro Definition
8.2 Macros: Tutorial
8.3 Destructuring
8.0 Introduction to macros
A macro in Common LISP is a facility which allows transformation of forms read in. The transformation is done on the forms right after they are read; not while they are being read, as in other languages. Macros are used in LISP primarily to improve the readability of code; they are also somewhat more efficient than function calls.
A macro call is a list which looks just like a function call. It is a list with the name of the macro at its head followed by a list of zero or more arguments. The difference is that a function call is processed by the evaluator; a macro call should never get to the evaluator.
The effect of processing a macro in Common LISP is as follows.
The element at the car of macro call list has been associated with a macro definition. This definition primarily consists of a function which takes two arguments, called the exapansion function. The effect is as if the expansion function were called with the entire macro call as the first argument and the current lexical environment as its second argument. The expansion function returns a new LISP form, which is known as the expansion of the macro call. The expansion of the macro call is then passed along for compilation and/or evaluation.
It is crucial that all relevant macro definitions are available, whether to the interpreter or compiler, before any code containing calls to those macros is introduced. Otherwise the macro calls will be considered function calls; subsequently, there will not be a functional definition for the form when it is evaluated.
It must be stressed that macros are not functions. Particularly, macros cannot be used as functional arguments to apply, funcall, or the map functions.
8.1 Macro Definition
The function macro-function determines whether a given symbol is the name of a macro. The defmacro construct provides a convenient way to define new macros.
macro-function
defmacro
Also take note of the Star Sapphire specific macro trace facility invoked by
mtrace
8.2 Macros: Tutorial
This section explains the Common LISP macro facility at an introductory level.
The defmacro facility allows the construction of new language elements. Mastering defmacro is an important step towards becomming a skilled LISP programmer.
Although a defmacro superficially looks like a defun, there are important differences, particularly when the form is invoked.
The first thing to understand about macros is that whereas functions evaluate all arguments (which are not otherwise blocked from being evaluated, by being quoted), macro arguments are never evaluated; they are exapanded into exactly what they are written as.
This is due to the fundamental difference between a macro and a function with regard to how it is implemented.
A macro call is exapanded, that is rewritten, prior to being compiled or evaluated. A function call is not normally transformed in any way. Therefore a given argument to a macro is substituted into the expansion of the macro.
As a very simple example, consider the following trivial defun:
(defun fmult(x y)
(* x y))
This returns the value of the product of its two arguments.
Here is a macro version which has the same value and effect:
(defmacro mmult(x y)
`(* ,x ,y))
(The ` and , will be explained shortly). Subsequently, an invocation of mmult:
(mmult 2 3)
gets expanded into the following form:
(* x y)
which returns
6
Why is it desireable to use a macro? The macro facility can implement facilities (such as setf and defstruct) which hide very complicated functionality and otherwise could not be done as defuns.
Furthermore, once a code expansion is performed, the generated code may be more time efficient. A function call in LISP can be more expensive compared to evaluating a piece of 'in-line' code, particularly if lambda list keywords are present in the parameter list and the argument list has to get analyzed prior to evaluation.
Now, that is this ` and , anyway? This is the backquote syntax; backquote is a Common LISP facility which is used to produce list structure; it is often used in conjunction with defmacro.
You should refer to the entry for backquote for the full story, but basically, the ` around a list expands into a form with append, that is it builds a list of its arguments, in much the same way that a 'forward quote' exapands into a list with quote at its head. The comma subsitutes a variable into the list form verbatim.
The form
`(* ,a ,b)
gets converted into something like
(append '* (list a)(list b))
which in turn produces the list structure
(* a b)
which when evaluated produces the expected result.
The procedure followed when expanding a macro is as follows: First the form is built into the body of a let* which has as its arguments the arguments to the macro call.
When this gets built, it will look like this (with the backquote expanded):
(let* ((b '3)(a '2))
(append '* (list a)(list b)))
This in turn is compiled and evaluated, which produces the form:
(* 2 3)
Finally, this form itself is evaluated which produces the desired result.
The second important thing to learn about defmacro is that, in effect, the body of the macro can be thought of as being evaluated twice; once at macro expansion time and again during the actual evalation.
The concept here is that a macro is not really a verbatim expansion with the arguments dropped into the body of the defmacro (as is the case with C macro functions, the only vaguely comparable construct to defmacro in a 'major' language).
The defmacro defines a form, which when expanded into a piece of code with the arguments substituted (as if by setting up a new lexical context using let*), produces the desired effect and values.
To build such a form we use the backquote syntax for readability.
If LISP macros worked like C macros then we could simply write mmult as:
(defmacro badmmult(x y) ; flawed
(* x y)) ; flawed
Note that in the simplest case, where the macro is applied to numeric arguments this works just like the mmult macro:
(badmmult 2 3) -> 6
However, let's set up some global variables:
(setq a 2 b 3)
and use them with badmmult:
(badmmult a b)
We get the following error:
expected number for argument 0, found A
LISP runtime error: type error on line 22, form 22
error in eval: line 22, form 22: (* A B)
In: * <- BLOCK <- LET* <- EVAL
To find out what is going on, invoke mtrace and watch how the different macros get expanded (it helps to have *print-pretty* set to t before doing this):
LISP: (mtrace)
T
LISP: (mmult 2 3)
expanding macro
(MMULT 2 3)
compiling macro
(LET* ((Y (QUOTE 3)) (X (QUOTE 2)))
(BLOCK MMULT (APPEND (QUOTE (*)) (LIST X) (LIST Y))))
evaluating macro
(LET* (CF-VARIABLE NIL X Y)
((#<LEXVAR 0:0> (QUOTE 2))(#<LEXVAR 0:1> (QUOTE 3)))
(BLOCK (CF-BLOCK MMULT)
(APPEND (QUOTE (*)) (LIST #<LEXVAR 1:1>) (LIST #<LEXVAR 1:0>))))
macro expansion is
(* 2 3)
6
LISP: (badmmult 2 3)
expanding macro
(BADMMULT 2 3)
compiling macro
(LET* ((Y (QUOTE 3)) (X (QUOTE 2)))
(BLOCK BADMMULT (* X Y)))
evaluating macro
(LET* (CF-VARIABLE NIL X Y)
((#<LEXVAR 0:0> (QUOTE 3)) (#<LEXVAR 0:1> (QUOTE 2)))
(BLOCK (CF-BLOCK BADMMULT)
(* #<LEXVAR 1:1> #<LEXVAR 1:0>)))
macro expansion is
6
6
LISP: (mmult a b)
expanding macro
(MMULT A B)
compiling macro
(LET* ((Y (QUOTE B)) (X (QUOTE A)))
(BLOCK MMULT (APPEND (QUOTE (*)) (LIST X) (LIST Y))))
evaluating macro
(LET* (CF-VARIABLE NIL X Y)
((#<LEXVAR 0:0> (QUOTE B)) (#<LEXVAR 0:1> (QUOTE A)))
(BLOCK (CF-BLOCK MMULT)
(APPEND (QUOTE (*)) (LIST #<LEXVAR 1:1>)
(LIST #<LEXVAR 1:0>))))
macro expansion is
(* A B)
12
LISP: (badmmult a b)
expanding macro
(BADMMULT A B)
compiling macro
(LET* ((Y (QUOTE B)) (X (QUOTE A)))
(BLOCK BADMMULT (* X Y)))
evaluating macro
(LET* (CF-VARIABLE NIL X Y)
((#<LEXVAR 0:0> (QUOTE B))(#<LEXVAR 0:1> (QUOTE A)))
(BLOCK (CF-BLOCK BADMMULT)
(* #<LEXVAR 1:1> #<LEXVAR 1:0>)))
expected number for argument 0, found A
LISP runtime error: type error on line 22, form 22
error in eval: line 22, form 22: (* A B)
In: * <- BLOCK <- LET* <- EVAL
Contrast the form
(* a b)
which produces what the result of multiplying a by b produces with the backquote example above. This works when you are dealing with constants, but fails when a and b are incorporated as quoted symbols into the form; the symbols themselves (which is what a produces in the let* variable list) do not have a numeric value; it is the value cell which has the numeric value (pursuant to the setq).
As you can see, the backquote syntax expands into a form which produces another piece of list structure which looks like the original backquote form if you ignore the ` and , symbols.
This simple example should give you a better idea of how the macro facility works and why the backquote syntax is useful in conjunction with it. You should try experimenting with defmacro with mtrace on to get more familiar with this important aspect of the language.
See also the penult example.
8.3 Destructuring
defmacro
, unlike any other Common LISP construct that has a lambda-list as part of its syntax, provides an additional facility known as destructuring. Anywhere in the lambda-list where a parameter name may appear, and where ordinary lambda-list syntax (as described in section 5.2.2.) does not otherwise allow a list, a lambda-list may appear in place of the parameter name. When this is done, then the argument form that would match the parameter is treated as a (possibly dotted) list, to be used as an argument forms list for satisfying the parameters in the embedded lambda-list. As an example, one could write the macro definition for dolist in this manner:(
defmacro dolist ((var listform &optional resultform)&rest body)
...)
More examples of embedded lambda-lists in defmacro are shown below.
Another destructuring rule is that defmacro allows any lambda-list (whether top-level or embedded) to be dotted, ending in a parameter name. This situation is treated exactly as if the parameter name that ends the list had appeared preceded by &rest. For example, the definition skeleton for dolist shown above could instead have been written
(
defmacro dolist ((var listform &optional resultform). body)
...)
If the compiler encounters a defmacro, the new macro is added to the compilation environment, and a compiled form of the expansion function is also added to the output file so that the new macro will be operative at runtime. If this is not the desired effect, the defmacro form can be wrapped in an eval-when construct.
It is permissible to use defmacro to redefine a macro (for example, to install a corrected version of an incorrect definition!), or to redefine a function as a macro. It is an error to attempt to redefine the name of a special form (see Table 5-1) as a macro. Suppose, for the sake of example, that it were desirable to implement a conditional construct analogous to the FORTRAN arithmetic IF statement. (This of course requires a certain stretching of the imagination and suspension of disbelief.) The construct should accept four forms: a test-value, a neg-form, a zero-form, and a pos-form. One of the last three forms is chosen to be executed according to whether the value of the test-form is positive, negative, or zero. Using defmacro, a definition for such a construct might look like this:
(
defmacro arithmetic-if (test neg-form zero-form pos-form)(
let ((var (gensym)))`(
let ((,var ,test))(
cond ((< ,var 0) ,neg-form)((= ,var 0) ,zero-form)
(
t ,pos-form)))))Note the use of the backquote facility in this definition. Also note the use of gensym to generate a new variable name. This is necessary to avoid conflict with any variables that might be referred to in neg-form, zero-form, or pos-form.
If the form is executed by the interpreter, it will cause the function definition of the symbol arithmetic-if to be a macro associated with which is a two-argument expansion function roughly equivalent to:
(lambda (calling-form environment)
(declare (ignore environment))
(let ((var (gensym)))
(list 'let
(list (list +var (cadr calling-form)))
(list 'cond
(list (list '< var +0) (caddr calling-form))
(list (list '= var +0) cadddr calling-form))
(list 't (fifth calling-form))))))
The lambda-expression is produced by the defmacro declaration. The calls to list are the (hypothetical) result of the backquote (`) macro character and its associated commas. The precise macro expansion function may depend on the implementation, for example providing some degree of explicit error checking on the number of argument forms in the macro call.
Now, if eval encounters
(arithmetic-if (- x 4.0)
(- x)
(error "Strange zero")
x)
this will be expanded into something like
(
let ((g407 (- x 4.0)))(
cond ((< g407 0) (- x))((
= g407 0) (error "Strange zero"))(
t x)))and eval tries again on this new form. (It should be clear now that the backquote facility is very useful in writing macros, since the form to be returned is normally a complex list structure, typically consisting of a mostly constant template with a few evaluated forms here and there. The backquote template provides a "picture" of the resulting code, with places to be filled in indicated by preceding commas.)
To expand on this example, stretching credibility to its limit, we might allow the pos-form and zero-form to be omitted, allowing their values to default to nil, in much the same way that the else form of a Common LISP if construct may be omitted:
(defmacro arithmetic-if (test neg-form
&optional zero-form pos-form)
(let ((var (gensym)))
`(let ((,var ,test))
(cond ((< ,var 0) ,neg-form)
((= ,var 0) ,zero-form)
(t ,pos-form)))))
Then one could write
(arithmetic-if (- x 4.0) (print x))
which would be expanded into something like
(let ((g408 (- x 4.0)))
(cond ((< g408 0) (print x))
((= g408 0) nil)
(t nil)))
The resulting code is correct but rather silly-looking. One might rewrite the macro definition to produce better code when pos-form and possibly zero-form are omitted, or one might simply rely on the Common LISP implementation to provide a compiler smart enough to improve the code itself.
Destructuring is a very powerful facility that allows the defmacro lambda-list to express the structure of a complicated macro-call syntax. If no lambda-list keywords appear, then the defmacro lambda-list is simply a list, nested to some extent, containing parameter names at the leaves. The macro-call form must have the same list structure. For example, consider this macro definition:
(defmacro halibut ((mouth eye1 eye2)
((fin1 length1) (fin2 length2))
tail)
...)
Now consider this macro call:
(halibut (m (car eyes) (cdr eyes))
((f1 (count-scales f1)) (f2 (count-scales f2)))
my-favorite-tail)
This would cause the expansion function to receive the following values for its parameters:
Parameter |
Value |
||
mouth |
m |
||
eye1 |
(car eyes) |
||
eye2 |
(cdr eyes) |
||
fin1 |
f1 |
||
length 1 |
(count-scales f1) |
||
fin2 |
f2 |
||
length 2 |
(count-scales f2) |
||
tail |
my-favorite-tail |
||
The following macro call would be in error because there would be no argument form to match the parameter length1:
(halibut (m (car eyes) (cdr eyes))
((f1) (f2 (count-scales f2)))
my-favorite-tail)
The following macro call would be in error because a symbol appears in the call where the structure of the lambda-list requires a list.
(halibut my-favorite-head
((f1 (count-scales f1)) (f2 (count-scales f2)))
my-favorite-tail)
The fact that the value of the variable my-favorite-head might happen to be a list is irrelevant here. It is the macro itself whose structure must match that of the defmacro lambda-list.
The use of lambda-list keywords adds even greater flexibility. For example, suppose it is convenient within the expansion function for halibut to be able to refer to the list whose components are called mouth, eye1, and eye2 as head. One may write this:
(defmacro halibut ((&whole head mouth eye1 eye2)
((fin 1 length 1) (fin2 length 2))
tail)
Now consider the same valid macro call as before:
(halibut (m (car eyes) (cdr eyes))
((f1 (count-scales f1)) (f2 (count-scales f2)))
my-favorite-tail)
This would cause the expansion function to receive the same values for its parameters and also a value for the parameter head:
Parameter Value
head (m (car eyes) (cdr eyes))
The stipulation, that an embedded lambda-list is permitted only where ordinary lambda-list syntax would permit a parameter name but not a list, is made to prevent ambiguity. For example, one may not write
(defmacro loser (x &optional (a b &rest c) &rest z)
...)
because ordinary lambda-list syntax does permit a list following &optional; the list (a b &rest c) would be interpreted as describing an optional parameter named a whole default value is that of the form b, with a supplied-p parameter named &rest (not legal), and an extraneous symbol c in the list (also not legal). An almost correct way to express this is
(defmacro loser (x &optional ((a b &rest c)) &rest z)
...)
The extra set of parentheses removes the ambiguity. However, the definition is now incorrect because a macro call such as (loser (carpool)) would not provide any argument form for the lambda-list (a b &rest c), and so the default value against which to match the lambda-list would be nil because no explicit default value was specified. This is in error because nil is an empty list; it does not have forms to satisfy the parameters a and b. The fully correct definition would be either
(defmacro loser (x &optional ((a b &rest c) '(nil nil)) &rest z)
...)
or
(defmacro loser (x &optional ((&optional a b &rest c)) &rest z) ...)
These differ slightly: The first requires that if the macro call specifies a explicitly then it must also specify b explicitly, whereas the second does not have this requirement. For example,
(loser (car pool) ((+ x 1)))
would be a valid call for the second definition but not for the first.