10.7 示例:实用的宏函数 (Example: Macro Utilities)

6.4 节介绍了实用函数 (utility)的概念,一种像是构造 Lisp 的通用操作符。我们可以使用宏来定义不能写作函数的实用函数。我们已经见过几个例子: nil! , ntimes 以及 while ,全部都需要写成宏,因为它们全都需要某种控制参数求值的方法。本节给出更多你可以使用宏写出的多种实用函数。图 10.2 挑选了几个实践中证实值得写的实用函数。

  1. (defmacro for (var start stop &body body)
  2. (let ((gstop (gensym)))
  3. `(do ((,var ,start (1+ ,var))
  4. (,gstop ,stop))
  5. ((> ,var ,gstop))
  6. ,@body)))
  7. (defmacro in (obj &rest choices)
  8. (let ((insym (gensym)))
  9. `(let ((,insym ,obj))
  10. (or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))
  11. choices)))))
  12. (defmacro random-choice (&rest exprs)
  13. `(case (random ,(length exprs))
  14. ,@(let ((key -1))
  15. (mapcar #'(lambda (expr)
  16. `(,(incf key) ,expr))
  17. exprs))))
  18. (defmacro avg (&rest args)
  19. `(/ (+ ,@args) ,(length args)))
  20. (defmacro with-gensyms (syms &body body)
  21. `(let ,(mapcar #'(lambda (s)
  22. `(,s (gensym)))
  23. syms)
  24. ,@body))
  25. (defmacro aif (test then &optional else)
  26. `(let ((it ,test))
  27. (if it ,then ,else)))

图 10.2: 实用宏函数

第一个 for ,设计上与 while 相似 (164 页,译注: 10.3 节)。它是给需要使用一个绑定至一个值的范围的新变量来对主体求值的循环:

  1. > (for x 1 8
  2. (princ x))
  3. 12345678
  4. NIL

这比写出等效的 do 来得省事,

  1. (do ((x 1 (+ x 1)))
  2. ((> x 8))
  3. (princ x))

这非常接近实际的展开式:

  1. (do ((x 1 (1+ x))
  2. (#:g1 8))
  3. ((> x #:g1))
  4. (princ x))

宏需要引入一个额外的变量来持有标记范围 (range)结束的值。 上面在例子里的 8 也可是个函数调用,这样我们就不需要求值好几次。额外的变量需要是一个 gensym ,为了避免非预期的变量捕捉。

图 10.2 的第二个宏 in ,若其第一个参数 eql 任何自己其他的参数时,返回真。表达式我们可以写成:

  1. (in (car expr) '+ '- '*)

我们可以改写成:

  1. (let ((op (car expr)))
  2. (or (eql op '+)
  3. (eql op '-)
  4. (eql op '*)))

确实,第一个表达式展开后像是第二个,除了变量 op 被一个 gensym 取代了。

下一个例子 random-choice ,随机选取一个参数求值。在 74 页 (译注: 第 4 章的图 4.6)我们需要随机在两者之间选择。 random-choice 宏实现了通用的解法。一个像是这样的调用:

  1. (random-choice (turn-left) (turn-right))

会被展开为:

  1. (case (random 2)
  2. (0 (turn-left))
  3. (1 (turn-right)))

下一个宏 with-gensyms 主要预期用在宏主体里。它不寻常,特别是在特定应用中的宏,需要 gensym 几个变量。有了这个宏,与其

  1. (let ((x (gensym)) (y (gensym)) (z (gensym)))
  2. ...)

我们可以写成

  1. (with-gensyms (x y z)
  2. ...)

到目前为止,图 10.2 定义的宏,没有一个可以定义成函数。作为一个规则,写成宏是因为你不能将它写成函数。但这个规则有几个例外。有时候你或许想要定义一个操作符来作为宏,好让它在编译期完成它的工作。宏 avg 返回其参数的平均值,

  1. > (avg 2 4 8)
  2. 14/3

是一个这种例子的宏。我们可以将 avg 写成函数,

  1. (defun avg (&rest args)
  2. (/ (apply #'+ args) (length args)))

但它会需要在执行期找出参数的数量。只要我们愿意放弃应用 avg ,为什么不在编译期调用 length 呢?

图 10.2 的最后一个宏是 aif ,它在此作为一个故意变量捕捉的例子。它让我们可以使用变量 it 来引用到一个条件式里的测试参数所返回的值。也就是说,与其写成

  1. (let ((val (calculate-something)))
  2. (if val
  3. (1+ val)
  4. 0))

我们可以写成

  1. (aif (calculate-something)
  2. (1+ it)
  3. 0)

小心使用 ( Use judiciously),预期的变量捕捉可以是一个无价的技巧。Common Lisp 本身在多处使用它: 举例来说 next-method-pcall-next-method 皆依赖于变量捕捉。

像这些宏明确演示了为何要撰写替你写程序的程序。一旦你定义了 for ,你就不需要写整个 do 表达式。值得写一个宏只为了节省打字吗?非常值得。节省打字是程序设计的全部;一个编译器的目的便是替你省下使用机械语言输入程序的时间。而宏允许你将同样的优点带到特定的应用里,就像高阶语言带给程序语言一般。通过审慎的使用宏,你也许可以使你的程序比起原来大幅度地精简,并使程序更显着地容易阅读、撰写及维护。

如果仍对此怀疑,考虑看看如果你没有使用任何内置宏时,程序看起来会是怎么样。所有宏产生的展开式,你会需要用手产生。你也可以将这个问题用在另一方面。当你在撰写一个程序时,扪心自问,我需要撰写宏展开式吗?如果是的话,宏所产生的展开式就是你需要写的东西。