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 generated by 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 once-only, which is used to generate code that evaluates certain macro arguments once only and in a particular order. Using once-only, you could write do-primes almost as simply as the original leaky version, like this:

(defmacro do-primes ((var start end) &body body)

(once-only (start end)

`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))

((> ,var ,end))

,@body)))

However, the implementation of once-only is a bit too involved for a blow-by-blow explanation, as it relies on multiple levels of backquoting and unquoting. If you really want to sharpen your macro chops, you can try to figure out how it works. It looks like this:

(defmacro once-only ((&rest names) &body body)

(let ((gensyms (loop for n in names collect (gensym))))

`(let (,@(loop for g in gensyms collect `(,g (gensym))))

`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))

,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))

,@body)))))

Beyond Simple Macros

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

Вы читаете Practical Common Lisp
Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату