Common Lisp 笔记: defmacro once-only
这两日在看 Practical Common Lisp。书相当不错,其中关于 macro 的讲解让我对 Lisp 的 macro 有了更多的了解。以前读 R5RS 的时候,每看到自定义宏总是当它是洪水猛兽,避之不及。一来是因为从来没有自己进行过语法层面的抽象。就像突然别人塞到手里一件宝贝,却完全不识货,难怪会心生畏惧。二来是不知道为何会有这样的需要,竟然要自己定义语法。习惯了让程序操纵数据,却不知道还可以让程序来编写程序。现在稍微好些,但还没有想得特别通透。
在书中举了一个例子,初看之下有点晕:
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for n in names collect (gensym))))
`(let (,@(loop for g in gensyms collect `(,g (gensym))))
`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
,@body)))))
如果把
(once-only (foo bar)
'(1 2 3))
展开,就是
(LET ((#:G1613 (GENSYM)) (#:G1614 (GENSYM)))
`(LET ((,#:G1613 ,FOO) (,#:G1614 ,BAR))
,(LET ((FOO #:G1613) (BAR #:G1614))
'(1 2 3))))
这里的 foo 和 bar 分别是两个将来的外部宏(即调用 once-only 的宏,比如书中的 do-primes)所使用的形参,它们对应的实参很可能是一些不希望被多次调用的表达式(如 (random 10) )。
once-only 的目的只有一个,让
(defmacro do-primes ((var start end) &body body)
(once-only (start end)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
(do-primes (i 1 (random 10)
(print i))
被展开成类似
(LET ((#:G1653 1) (#:G1654 (RANDOM 10)))
(DO ((I (NEXT-PRIME #:G1653) (NEXT-PRIME (1+ I)))) ((> I #:G1654))
(PRINT I)))
的语句。要完成这个任务, once-only 要做两件事情:
- 绑定两个新变量,变量名由 (gensym) 得出,让它们的值为实参元素 start 和 end 分别 evaluate 得到的结果。
- 把 do-primes 的 body 中所有出现 start 和 end 的地方都替换为这两个新变量名。
与其复杂的假象相反,其实, once-only 相当直截了当地完成了这两件事。
-
(let ((gensyms (loop for n in names collect (gensym)))) `(let (,@(loop for g in gensyms collect `(,g (gensym))))
在这里,定义了 len(names) 个匿名变量,它们的值都是 gensym 生成的。虽然这些变量的变量名也是 gensym 生成的,但是我们所关心的并不是它们的名字,而是它们的值。这两句话在经过 once-only 和 do-primes 两次展开后就不复存在了,第一次展开会吃掉第一行语句,第二次则会吃掉第二行语句,它们留下的只有 gensyms 这个列表中每个元素对应的值,它们才是前面提到的“新变量”的变量名。
-
``(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
为了看得更清楚一些,我把外面的 backquote 加上了。这句话是会留在展开后的 do-primes 里的。它把 names 中的元素 n 所 evaluate 出的值绑定到了新的变量名上。
-
现在只有一件事了,让 do-primes 使用新的变量名而非 start 和 end。
``(...... ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
这里干脆把新变量名绑定到了老变量名上,这样处理的话,在展开 do-primes 宏的过程中,所有的老变量名就都会被替换成新的名字。是的,这句话也不会出现在二次展开的最后结果中。
好了,总算解释完了。咦,我怎么突然有种似曾相识的感觉?
