Macro-Writing Macros(生成宏的宏)

Of course, there's no reason you should be able to take advantage of macros only when writing functions. The job of macros is to abstract away common syntactic patterns, and certain patterns come up again and again in writing macros that can also benefit from being abstracted away.

当然,没有理由表明只有在编写函数的时候才能利用宏的优势。宏的作用是将常见的句法模式抽象掉,而反复出现在宏的编写中的特定模式同样也可受益于其抽象能力。

In fact, you've already seen one such pattern--many macros will, like the last version of do-primes, start with a LET that introduces a few variables holding gensymed symbols to be used in the macro's expansion. Since this is such a common pattern, why not abstract it away with its own macro?

事实上,你已经见过了这样一种模式。许多宏,例如最后版本的 do-primes,它们都以一个 LET 形式开始,后者引入了一些变量用来保存宏展开过程中用到的生成符号。由于这也是一个常见模式,那为什么不用一个宏来将其抽象掉呢?

In this section you'll write a macro, with-gensyms, that does just that. In other words, you'll write a macro-writing macro: a macro that generates code that generates code. While complex macro-writing 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.

本节将编写一个宏 with-gensyms,它刚好做到这点。换句话说,你将编写一个用来编写宏的宏:一个宏用来生成代码,其代码又生成另外的代码。尽管在你习惯于在头脑中牢记不同层次的代码之前,可能会对复杂的编写宏的宏有一点困惑,但 with-gensyms 是相当简单的,而且还当作可以一个有用但又不会过于浪费脑筋的练习。

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.

并且还需要让其等价于之前版本的 do-primes。换句话说,with-gensyms 需要展开成一个 LET,它会把每一个命名的变量(在本例中是 ending-value-name)都绑定到一个生成符号上。很容易就可以写出一个简单的反引用模板。

(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.

注意你是怎样用一个逗号来插入 LOOP 表达式的值的。这个循环生成了一个绑定形式的列表,其中每个绑定形式由一个含有 with-gensyms 中的一个给定名字和字面代码 (gensym) 的列表所构成。你可以通过将 names 替换成一个符号的列表,从而在 REPL 中测试 LOOP 表达式生成的代码。

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.

在绑定形式的列表之后,with-gensyms 的主体参数被嵌入到LET的主体之中。这样,被封装在一个 with-gensyms 中的代码将可以引用任何传递给 with-gensyms 的变量列表中所命名的变量。

If you macro-expand the with-gensyms form in the new definition of do-primes, you should see something like this:

如果在新的 do-primes 定义中对 with-gensyms 形式进行宏展开,就将看到下面这样的结果:

(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.

看起来不错。尽管这个宏相对简单,但重要的是要清楚地了解不同的宏是分别在何时被展开的:当你编译关于 do-primesDEFMACRO 时,with-gensyms 形式就被展开成刚刚看到的代码并被编译了。这样,do-primes 的编译版本就已经跟你手写外层的 LET 时一样了。当编译一个使用了 do-primes 的函数时,由 with-gensyms 生成的代码将会运行用来生成 do-primes 的展开式,但 with-gensyms 宏本身在编译一个 do-primes 形式时并不会被用到,因为在 do-primes 被编译时,它早已经被展开了。