图 10.2: 实用宏函数
第一个 for
,设计上与 while
相似 (164 页,译注: 10.3 节)。它是给需要使用一个绑定至一个值的范围的新变量来对主体求值的循环:
> (for x 1 8
(princ x))
12345678
NIL
这比写出等效的 do
来得省事,
(do ((x 1 (+ x 1)))
((> x 8))
(princ x))
这非常接近实际的展开式:
(do ((x 1 (1+ x))
((> x #:g1))
(princ x))
宏需要引入一个额外的变量来持有标记范围 (range)结束的值。 上面在例子里的 也可是个函数调用,这样我们就不需要求值好几次。额外的变量需要是一个 gensym ,为了避免非预期的变量捕捉。
图 10.2 的第二个宏 in
,若其第一个参数 eql
任何自己其他的参数时,返回真。表达式我们可以写成:
(let ((op (car expr)))
(or (eql op '+)
(eql op '-)
(eql op '*)))
确实,第一个表达式展开后像是第二个,除了变量 op
被一个 gensym 取代了。
下一个例子 random-choice
,随机选取一个参数求值。在 74 页 (译注: 第 4 章的图 4.6)我们需要随机在两者之间选择。 random-choice
宏实现了通用的解法。一个像是这样的调用:
(random-choice (turn-left) (turn-right))
会被展开为:
(case (random 2)
(1 (turn-right)))
下一个宏 with-gensyms
主要预期用在宏主体里。它不寻常,特别是在特定应用中的宏,需要 gensym 几个变量。有了这个宏,与其
我们可以写成
(with-gensyms (x y z)
...)
到目前为止,图 10.2 定义的宏,没有一个可以定义成函数。作为一个规则,写成宏是因为你不能将它写成函数。但这个规则有几个例外。有时候你或许想要定义一个操作符来作为宏,好让它在编译期完成它的工作。宏 返回其参数的平均值,
> (avg 2 4 8)
14/3
(defun avg (&rest args)
(/ (apply #'+ args) (length args)))
但它会需要在执行期找出参数的数量。只要我们愿意放弃应用 avg
,为什么不在编译期调用 length
呢?
图 10.2 的最后一个宏是 aif
,它在此作为一个故意变量捕捉的例子。它让我们可以使用变量 it
来引用到一个条件式里的测试参数所返回的值。也就是说,与其写成
我们可以写成
(aif (calculate-something)
0)
小心使用 ( Use judiciously),预期的变量捕捉可以是一个无价的技巧。Common Lisp 本身在多处使用它: 举例来说 next-method-p
与 call-next-method
皆依赖于变量捕捉。
像这些宏明确演示了为何要撰写替你写程序的程序。一旦你定义了 for
,你就不需要写整个 do
表达式。值得写一个宏只为了节省打字吗?非常值得。节省打字是程序设计的全部;一个编译器的目的便是替你省下使用机械语言输入程序的时间。而宏允许你将同样的优点带到特定的应用里,就像高阶语言带给程序语言一般。通过审慎的使用宏,你也许可以使你的程序比起原来大幅度地精简,并使程序更显着地容易阅读、撰写及维护。
如果仍对此怀疑,考虑看看如果你没有使用任何内置宏时,程序看起来会是怎么样。所有宏产生的展开式,你会需要用手产生。你也可以将这个问题用在另一方面。当你在撰写一个程序时,扪心自问,我需要撰写宏展开式吗?如果是的话,宏所产生的展开式就是你需要写的东西。