(list (list (list var
(list 'next-prime start)
(list 'next-prime (list '1+ var)))))
(list (list (list '> var end)))
body))
As you'll see in a moment, the current implementation of do-primes
doesn't handle certain edge cases correctly. But first you should verify that it at least works for the original example. You can test it in two ways. You can test it indirectly by simply using it—presumably, if the resulting behavior is correct, the expansion is correct. For instance, you can type the original example's use of do-primes
to the REPL and see that it indeed prints the right series of prime numbers.
CL-USER> (do-primes (p 0 19) (format t '~d ' p))
2 3 5 7 11 13 17 19
NIL
Or you can check the macro directly by looking at the expansion of a particular call. The function MACROEXPAND-1
takes any Lisp expression as an argument and returns the result of doing one level of macro expansion.[95] Because MACROEXPAND-1
is a function, to pass it a literal macro form you must quote it. You can use it to see the expansion of the previous call.[96]
CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t '~d ' p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P 19))
(FORMAT T '~d ' P))
T
Or, more conveniently, in SLIME you can check a macro's expansion by placing the cursor on the opening parenthesis of a macro form in your source code and typing C-c RET
to invoke the Emacs function slime-macroexpand-1
, which will pass the macro form to MACROEXPAND- 1
and 'pretty print' the result in a temporary buffer.
However you get to it, you can see that the result of macro expansion is the same as the original handwritten expansion, so it seems that do-primes
works.
In his essay 'The Law of Leaky Abstractions,' Joel Spolsky coined the term
As it turns out, a macro can leak details of its inner workings in three ways. Luckily, it's pretty easy to tell whether a given macro suffers from any of those leaks and to fix them.
The current definition suffers from one of the three possible macro leaks: namely, it evaluates the end
subform too many times. Suppose you were to call do-primes
with, instead of a literal number such as 19
, an expression such as (random 100)
in the end
position.
(do-primes (p 0 (random 100))
(format t '~d ' p))
Presumably the intent here is to loop over the primes from zero to whatever random number is returned by (random 100)
. However, this isn't what the current implementation does, as MACROEXPAND-1
shows.
CL-USER> (macroexpand-1 '(do-primes (p 0 (random 100)) (format t '~d ' p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P (RANDOM 100)))
(FORMAT T '~d ' P))
T
When this expansion code is run, RANDOM
will be called each time the end test for the loop is evaluated. Thus, instead of looping until p
is greater than an initially chosen random number, this loop will iterate until it happens to draw a random number less than or equal to the current value of p
. While the total number of iterations will still be random, it will be drawn from a much different distribution than the uniform distribution RANDOM
returns.
This is a leak in the abstraction because, to use the macro correctly, the caller needs to be aware that the end
form is going to be evaluated more than once. One way to plug this leak would be to simply define this as the behavior of do-primes
. But that's not very satisfactory—you should try to observe the Principle of Least Astonishment when implementing macros. And programmers will typically expect the forms they pass to macros to be evaluated no more times than absolutely necessary.[98] Furthermore, since do-primes
is built on the model of the standard macros, DOTIMES
and DOLIST
, neither of which causes any of the forms except those in the body to be evaluated more than once, most programmers will expect do-primes
to behave similarly.
You can fix the multiple evaluation easily enough; you just need to generate code that evaluates end
once and saves the value in a variable to be used later. Recall that in a DO
loop, variables defined with an initialization form and no step form don't change from iteration to iteration. So you can fix the multiple evaluation problem with this definition:
(defmacro do-primes ((var start end) &body body)
`(do ((ending-value ,end)
(,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ending-value))
,@body))
Unfortunately, this fix introduces two new leaks to the macro abstraction.
One new leak is similar to the multiple-evaluation leak you just fixed. Because the initialization forms for variables in a DO
loop are evaluated in the order the variables are defined, when the macro expansion is evaluated, the expression passed as end
will be evaluated before the expression passed as start
, opposite to the order they appear in the macro call. This leak doesn't cause any problems when start
and end
are literal values like 0 and 19. But when they're forms that can have side effects, evaluating them out of order can once again run afoul of the Principle of Least Astonishment.
This leak is trivially plugged by swapping the order of the two variable definitions.
(defmacro do-primes ((var start end) &body body)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(ending-value ,end))
((> ,var ending-value))
,@body))
The last leak you need to plug was created by using the variable name ending-value
. The problem is that the name, which ought to be a purely internal detail of the macro implementation, can end up interacting with code passed to the macro or in the context where the macro is called. The following seemingly innocent call to do-primes
doesn't work correctly because of this leak:
(do-primes (ending-value 0 10)
(print ending-value))
Neither does this one:
(let ((ending-value 0))