本节快速给出宏所牵涉问题的概要,以及解决它们的技巧。作为一个例子,我们会定义一个称为 的宏,它接受一个数字 n 并对其主体求值 n 次。

    下面是一个不正确的 ntimes 定义,说明了宏设计中的某些议题:

    1. (defmacro ntimes (n &rest body)
    2. `(do ((x 0 (+ x 1)))
    3. ((>= x ,n))
    4. ,@body))

    这个定义第一眼看起来可能没问题。在上面这个情况,它会如预期的工作。但实际上它在两个方面坏掉了。

    一个宏设计者需要考虑的问题之一是,不小心引入的变量捕捉 (variable capture)。这发生在当一个在宏展开式里用到的变量,恰巧与展开式即将插入的语境里,有使用同样名字作为变量的情况。不正确的 ntimes 定义创造了一个变量 x 。所以如果这个宏在已经有 x 作为名字的地方被调用时,它可能无法做到我们所预期的:

    1. > (let ((x 10))
    2. (ntimes 5
    3. (setf x (+ x 1)))
    4. x)
    5. 10

    最普遍的解法是不要使用任何可能会被捕捉的一般符号。取而代之的我们使用 gensym (8.4 小节)。因为 read 函数 intern 每个它见到的符号,所以在一个程序里,没有可能会有任何符号会 eql gensym。如果我们使用 gensym 而不是 x 来重写 ntimes 的定义,至少对于变量捕捉来说,它是安全的:

    1. (defmacro ntimes (n &rest body)
    2. `(do ((,g 0 (+ ,g 1)))
    3. ((>= ,g ,n))
    4. ,@body)))

    但这个宏在另一问题上仍有疑虑: 多重求值 (multiple evaluation)。因为第一个参数被直接插入 do 表达式,它会在每次迭代时被求值。当第一个参数是有副作用的表达式,这个错误非常清楚地表现出来:

    1. > (let ((v 10))
    2. (ntimes (setf v (- v 1))
    3. (princ ".")))
    4. .....
    5. NIL

    由于 v 一开始是 10 ,而 返回其第二个参数的值,应该印出九个句点。实际上它只印出五个。

    如果我们看看宏调用所展开的表达式,就可以知道为什么:

    避免非预期的多重求值的方法是设置一个变量,在任何迭代前将其设为有疑惑的那个表达式。这通常牵扯到另一个 gensym:

    1. (defmacro ntimes (n &rest body)
    2. (let ((g (gensym))
    3. `(let ((,h ,n))
    4. (do ((,g 0 (+ ,g 1)))
    5. ((>= ,g ,h))
    6. ,@body))))

    终于,这是一个 ntimes 的正确定义。

    非预期的变量捕捉与多重求值是折磨宏的主要问题,但不只有这些问题而已。有经验后,要避免这样的错误与避免更熟悉的错误一样简单,比如除以零的错误。

    你的 Common Lisp 实现是一个学习更多有关宏的好地方。借由调用展开至内置宏,你可以理解它们是怎么写的。下面是大多数实现对于一个 cond 表达式会产生的展开式:

    1. > (pprint (macroexpand-1 '(cond (a b)
    2. (c d e)
    3. (t f))))
    4. (IF A
    5. B
    6. (IF C