本节快速给出宏所牵涉问题的概要,以及解决它们的技巧。作为一个例子,我们会定义一个称为 的宏,它接受一个数字 n 并对其主体求值 n 次。
下面是一个不正确的 ntimes
定义,说明了宏设计中的某些议题:
(defmacro ntimes (n &rest body)
`(do ((x 0 (+ x 1)))
((>= x ,n))
,@body))
这个定义第一眼看起来可能没问题。在上面这个情况,它会如预期的工作。但实际上它在两个方面坏掉了。
一个宏设计者需要考虑的问题之一是,不小心引入的变量捕捉 (variable capture)。这发生在当一个在宏展开式里用到的变量,恰巧与展开式即将插入的语境里,有使用同样名字作为变量的情况。不正确的 ntimes
定义创造了一个变量 x
。所以如果这个宏在已经有 x
作为名字的地方被调用时,它可能无法做到我们所预期的:
> (let ((x 10))
(ntimes 5
(setf x (+ x 1)))
x)
10
最普遍的解法是不要使用任何可能会被捕捉的一般符号。取而代之的我们使用 gensym (8.4 小节)。因为 read
函数 intern
每个它见到的符号,所以在一个程序里,没有可能会有任何符号会 eql
gensym。如果我们使用 gensym 而不是 x
来重写 ntimes
的定义,至少对于变量捕捉来说,它是安全的:
(defmacro ntimes (n &rest body)
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))
但这个宏在另一问题上仍有疑虑: 多重求值 (multiple evaluation)。因为第一个参数被直接插入 do
表达式,它会在每次迭代时被求值。当第一个参数是有副作用的表达式,这个错误非常清楚地表现出来:
> (let ((v 10))
(ntimes (setf v (- v 1))
(princ ".")))
.....
NIL
由于 v
一开始是 10
,而 返回其第二个参数的值,应该印出九个句点。实际上它只印出五个。
如果我们看看宏调用所展开的表达式,就可以知道为什么:
避免非预期的多重求值的方法是设置一个变量,在任何迭代前将其设为有疑惑的那个表达式。这通常牵扯到另一个 gensym:
(defmacro ntimes (n &rest body)
(let ((g (gensym))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))
终于,这是一个 ntimes
的正确定义。
非预期的变量捕捉与多重求值是折磨宏的主要问题,但不只有这些问题而已。有经验后,要避免这样的错误与避免更熟悉的错误一样简单,比如除以零的错误。
你的 Common Lisp 实现是一个学习更多有关宏的好地方。借由调用展开至内置宏,你可以理解它们是怎么写的。下面是大多数实现对于一个 cond
表达式会产生的展开式:
> (pprint (macroexpand-1 '(cond (a b)
(c d e)
(t f))))
(IF A
B
(IF C