Classic Macros
11.3 Conditional Evaluation
Sometimes we want an argument in a macro call to be evaluated only under certain conditions. This is beyond the ability of functions, which always evaluate all their arguments. Built-in operators like if, and, and cond protect some of their arguments from evaluation unless other arguments return certain values. For example, in this expression
(if t
’phew (/ x 0))
the third argument would cause a division-by-zero error if it were evaluated. But since only the first two arguments ever will be evaluated, the if as a whole will always safely return phew.
We can create new operators of this sort by writing macros which expand into calls to the existing ones. The two macros in Figure 11.5 are two of many possible
11.3 CONDITIONAL EVALUATION 151
variations on if. The definition of if3 shows how we could define a conditional for a three-valued logic. Instead of treating nil as false and everything else as true, this macro considers three categories of truth: true, false, and uncertain, represented as ?. It might be used as in the following description of a five year-old:
(while (not sick) (if3 (cake-permitted)
(eat-cake)
(throw ’tantrum nil) (plead-insistently)))
The new conditional expands into a case. (The nil key has to be enclosed within a list because a nil key alone would be ambiguous.) Only one of the last three arguments will be evaluated, depending on the value of the first.
The name nif stands for “numeric if.” Another implementation of this macro appeared on page 86. It takes a numeric expression as its first argument, and depending on its sign evaluates one of the remaining three arguments.
> (mapcar #’(lambda (x)
(nif x ’p ’z ’n))
’(0 1 -1)) (Z P N)
Figure 11.6 contains several more macros which take advantage of conditional evaluation. The macro in is to test efficiently for set membership. When you want to test whether an object is one of a set of alternatives, you could express the query as a disjunction:
(let ((x (foo)))
(or (eql x (bar)) (eql x (baz)))) or you could express it in terms of set membership:
(member (foo) (list (bar) (baz)))
The latter is more abstract, but less efficient. The member expression incurs unnecessary costs from two sources. It conses, because it must assemble the alternatives into a list for member to search. And to form the alternatives into a list they all have to be evaluated, even though some of the values may never be needed. If the value of (foo) is equal to the value of (bar), then there is no need to evaluate (baz). Whatever its conceptual advantages, this is not a good way to use member. We can get the same abstraction more efficiently with a macro: in combines the abstraction of member with the efficiency of or. The equivalent in expression
(defmacro in (obj &rest choices) (let ((insym (gensym)))
‘(let ((,insym ,obj))
(or ,@(mapcar #’(lambda (c) ‘(eql ,insym ,c)) choices)))))
(defmacro inq (obj &rest args)
‘(in ,obj ,@(mapcar #’(lambda (a)
‘’,a) args))) (defmacro in-if (fn &rest choices)
(let ((fnsym (gensym)))
‘(let ((,fnsym ,fn))
(or ,@(mapcar #’(lambda (c)
‘(funcall ,fnsym ,c)) choices)))))
(defmacro >case (expr &rest clauses) (let ((g (gensym)))
‘(let ((,g ,expr))
(cond ,@(mapcar #’(lambda (cl) (>casex g cl)) clauses)))))
(defun >casex (g cl)
(let ((key (car cl)) (rest (cdr cl)))
(cond ((consp key) ‘((in ,g ,@key) ,@rest)) ((inq key t otherwise) ‘(t ,@rest)) (t (error "bad >case clause")))))
Figure 11.6: Macros for conditional evaluation.
(in (foo) (bar) (baz))
has the same shape as the member expression, but expands into (let ((#:g25 (foo)))
(or (eql #:g25 (bar)) (eql #:g25 (baz))))
As is often the case, when faced with a choice between a clean idiom and an efficient one, we go between the horns of the dilemma by writing a macro which
11.4 CONDITIONAL EVALUATION 153
transforms the former into the latter.
Pronounced “in queue,” inq is a quoting variant of in, as setq used to be of set. The expression
(inq operator + - *) expands into
(in operator ’+ ’- ’*)
As member does by default, in and inq use eql to test for equality. When you want to use some other test—or any other function of one argument—you can use the more general in-if. What in is to member, in-if is to some. The expression
(member x (list a b) :test #’equal) can be duplicated by
(in-if #’(lambda (y) (equal x y)) a b) and
(some #’oddp (list a b)) becomes
(in-if #’oddp a b)
Using a combination of cond and in, we can define a useful variant of case.
The Common Lisp case macro assumes that its keys are constants. Sometimes we may want the behavior of a case expression, but with keys which are evaluated.
For such situations we define >case, like case except that the keys guarding each clause are evaluated before comparison. (The > in the name is intended to suggest the arrow notation used to represent evaluation.) Because >case uses in, it evaluates no more of the keys than it needs to.
Since keys can be Lisp expressions, there is no way to tell if (x y) is a call or a list of two keys. To avoid ambiguity, keys (other than t and otherwise) must always be given in a list, even if there is only one of them. In case expressions, nil may not appear as the car of a clause on grounds of ambiguity. In a >case expression, nil is no longer ambiguous as the car of a clause, but it does mean that the rest of the clause will never be evaluated.
For clarity, the code that generates the expansion of each >case clause is defined as a separate function, >casex. Notice that >casex itself uses inq.
(defmacro while (test &body body)
‘(do ()
((not ,test)) ,@body))
(defmacro till (test &body body)
‘(do () (,test) ,@body))
(defmacro for ((var start stop) &body body) (let ((gstop (gensym)))
‘(do ((,var ,start (1+ ,var)) (,gstop ,stop))
((> ,var ,gstop)) ,@body)))
Figure 11.7: Simple iteration macros.
11.4 Iteration
Sometimes the trouble with functions is not that their arguments are always evaluated, but that they are evaluated only once. Because each argument to a function will be evaluated exactly once, if we want to define an operator which takes some body of expressions and iterates through them, we will have to define it as a macro.
The simplest example would be a macro which evaluated its arguments in sequence forever:
(defmacro forever (&body body)
‘(do () (nil) ,@body))
This is just what the built-in loop macro does if you give it no loop keywords. It might seem that there is not much future (or too much future) in looping forever.
But combined with block and return-from, this kind of macro becomes the most natural way to express loops where termination is always in the nature of an emergency.
Some of the simplest macros for iteration are shown in Figure 11.7. We have already seen while (page 91), whose body will be evaluated while a test
11.4 ITERATION 155
expression returns true. Its converse is till, which does the same while a test expression returns false. Finally for, also seen before (page 129), iterates for a range of numbers.
By defining these macros to expand into dos, we enable the use of go and return within their bodies. As do inherits these rights from block and tagbody, while, till, and for inherit them from do. As explained on page 131, the nil tag of the implicit block around do will be captured by the macros defined in Figure 11.7. This is more of a feature than a bug, but it should at least be mentioned explicitly.
Macros are indispensable when we need to define more powerful iteration constructs. Figure 11.8 contains two generalizations of dolist; both evaluate their body with a tuple of variables bound to successive subsequences of a list.
For example, given two parameters, do-tuples/o will iterate by pairs:
> (do-tuples/o (x y) ’(a b c d) (princ (list x y)))
(A B)(B C)(C D) NIL
Given the same arguments, do-tuples/c will do the same thing, then wrap around to the front of the list:
> (do-tuples/c (x y) ’(a b c d) (princ (list x y)))
(A B)(B C)(C D)(D A) NIL
Both macros return nil, unless an explicit return occurs within the body.
This kind of iteration is often needed in programs which deal with some notion of a path. The suffixes /o and /c are intended to suggest that the two versions traverse open and closed paths, respectively. For example, if points is a list of points and (drawline x y) draws the line between x and y, then to draw the path from the first point to the last we write.
(do-tuples/o (x y) points (drawline x y))
whereas, if points is a list of the vertices of a polygon, to draw its perimeter we write
(do-tuples/c (x y) points (drawline x y))
The list of parameters given as the first argument can be any length, and iteration will proceed by tuples of that length. If just one parameter is given, both
(defmacro do-tuples/o (parms source &body body) (if parms
(let ((src (gensym)))
‘(prog ((,src ,source))
(mapc #’(lambda ,parms ,@body) ,@(map0-n #’(lambda (n)
‘(nthcdr ,n ,src)) (1- (length parms)))))))) (defmacro do-tuples/c (parms source &body body)
(if parms
(with-gensyms (src rest bodfn) (let ((len (length parms)))
‘(let ((,src ,source))
(when (nthcdr ,(1- len) ,src) (labels ((,bodfn ,parms ,@body))
(do ((,rest ,src (cdr ,rest))) ((not (nthcdr ,(1- len) ,rest))
,@(mapcar #’(lambda (args)
‘(,bodfn ,@args)) (dt-args len rest src)) nil)
(,bodfn ,@(map1-n #’(lambda (n)
‘(nth ,(1- n) ,rest)) len)))))))))) (defun dt-args (len rest src)
(map0-n #’(lambda (m)
(map1-n #’(lambda (n)
(let ((x (+ m n))) (if (>= x len)
‘(nth ,(- x len) ,src)
‘(nth ,(1- x) ,rest)))) len))
(- len 2)))
Figure 11.8: Macros for iteration by subsequences.
11.4 ITERATION 157
(do-tuples/c (x y z) ’(a b c d) (princ (list x y z)))
expands into:
(let ((#:g2 ’(a b c d))) (when (nthcdr 2 #:g2)
(labels ((#:g4 (x y z)
(princ (list x y z)))) (do ((#:g3 #:g2 (cdr #:g3)))
((not (nthcdr 2 #:g3)) (#:g4 (nth 0 #:g3)
(nth 1 #:g3) (nth 0 #:g2)) (#:g4 (nth 1 #:g3)
(nth 0 #:g2) (nth 1 #:g2)) nil)
(#:g4 (nth 0 #:g3) (nth 1 #:g3) (nth 2 #:g3))))))
Figure 11.9: Expansion of a call to do-tuples/c.
degenerate to dolist:
> (do-tuples/o (x) ’(a b c) (princ x)) ABC
NIL
> (do-tuples/c (x) ’(a b c) (princ x)) ABC
NIL
The definition of do-tuples/c is more complex than that of do-tuples/o, because it has to wrap around on reaching the end of the list. If there are n parameters, do-tuples/c must do n−1 more iterations before returning:
> (do-tuples/c (x y z) ’(a b c d) (princ (list x y z)))
(A B C)(B C D)(C D A)(D A B) NIL
> (do-tuples/c (w x y z) ’(a b c d) (princ (list w x y z)))
(A B C D)(B C D A)(C D A B)(D A B C) NIL
The expansion of the former call to do-tuples/c is shown in Figure 11.9. The hard part to generate is the sequence of calls representing the wrap around to the front of the list. These calls (in this case, two of them) are generated by dt-args.