macros can be a bit confusing until you get used to keeping the various levels of code clear in your mind, with-gensyms
is fairly straightforward and will serve as a useful but not too strenuous mental limbering exercise.
You want to be able to write something like this:
(defmacro do-primes ((var start end) &body body)
(with-gensyms (ending-value-name)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
and have it be equivalent to the previous version of do-primes
. In other words, the with-gensyms
needs to expand into a LET
that binds each named variable, ending-value-name
in this case, to a gensymed symbol. That's easy enough to write with a simple backquote template.
(defmacro with-gensyms ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
Note how you can use a comma to interpolate the value of the LOOP
expression. The loop generates a list of binding forms where each binding form consists of a list containing one of the names given to with-gensyms
and the literal code (gensym)
. You can test what code the LOOP
expression would generate at the REPL by replacing names
with a list of symbols.
CL-USER> (loop for n in '(a b c) collect `(,n (gensym)))
((A (GENSYM)) (B (GENSYM)) (C (GENSYM)))
After the list of binding forms, the body argument to with-gensyms
is spliced in as the body of the LET
. Thus, in the code you wrap in a with-gensyms
you can refer to any of the variables named in the list of variables passed to with-gensyms
.
If you macro-expand the with-gensyms
form in the new definition of do- primes
, you should see something like this:
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body))
Looks good. While this macro is fairly trivial, it's important to keep clear about when the different macros are expanded: when you compile the DEFMACRO
of do-primes
, the with-gensyms
form is expanded into the code just shown and compiled. Thus, the compiled version of do-primes
is just the same as if you had written the outer LET
by hand. When you compile a function that uses do- primes
, the code with-gensyms
runs generating the do-primes
expansion, but with-gensyms
itself isn't needed to compile a do-primes
form since it has already been expanded, back when do-primes
was compiled.
Another classic macro-writing MACRO: ONCE-ONLY |
Another classic macro-writing macro is
However, the implementation of
|
I could, of course, say a lot more about macros. All the macros you've seen so far have been fairly simple examples that save you a bit of typing but don't provide radical new ways of expressing things. In upcoming chapters you'll see examples of macros that allow you to express things in ways that would be virtually impossible without macros. You'll start in the very next chapter, in which you'll build a simple but effective unit test framework.
9. Practical: Building a Unit Test Framework
In this chapter you'll return to cutting code and develop a simple unit testing framework for Lisp. This will give you a chance to use some of the features you've learned about since Chapter 3, including macros and dynamic variables, in real code.
The main design goal of the test framework will be to make it as easy as possible to add new tests, to run various suites of tests, and to track down test failures. For now you'll focus on designing a framework you can use during interactive development.
The key feature of an automated testing framework is that the framework is responsible for telling you whether all the tests passed. You don't want to spend your time slogging through test output checking answers when the computer can do it much more quickly and accurately. Consequently, each test case must be an expression that yields a boolean value—true or false, pass or fail. For instance, if you were writing tests for the built-in +
function, these might be reasonable test cases:[100]
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)
Functions that have side effects will be tested slightly differently—you'll have to call the function and then check for evidence of the expected side effects.[101] But in the end, every