Tchaikov’s Journal

June 8, 2007

Common Lisp 笔记: defmacro once-only

Filed under: Programming

这两日在看 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 要做两件事情:

  1. 绑定两个新变量,变量名由 (gensym) 得出,让它们的值为实参元素 start 和 end 分别 evaluate 得到的结果。
  2. 把 do-primes 的 body 中所有出现 start 和 end 的地方都替换为这两个新变量名。

与其复杂的假象相反,其实, once-only 相当直截了当地完成了这两件事。

  1. (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 这个列表中每个元素对应的值,它们才是前面提到的“新变量”的变量名。

  2. ``(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
    

    为了看得更清楚一些,我把外面的 backquote 加上了。这句话是会留在展开后的 do-primes 里的。它把 names 中的元素 n 所 evaluate 出的值绑定到了新的变量名上。

  3. 现在只有一件事了,让 do-primes 使用新的变量名而非 start 和 end。

    ``(...... ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
    

    这里干脆把新变量名绑定到了老变量名上,这样处理的话,在展开 do-primes 宏的过程中,所有的老变量名就都会被替换成新的名字。是的,这句话也不会出现在二次展开的最后结果中。

好了,总算解释完了。咦,我怎么突然有种似曾相识的感觉?

老同学碰面

Filed under: Life

手续一办完,刚闲下来,就马不停蹄去了北京。找老同学去!

遇到的几位都没有大变。要说这是因为一两年之内还见过面,那么欧哥已经有整整十年没有见过其真人或者照片了。不过一样如高三时憨态可掬。要说有变化,就是大多数人或已经组建家庭,或更进一步准备下一代了。

几个朋友聚到一起开始海阔天空到一定阶段,话题自然而然地就收敛到我身上,不是因为我是唯一的非在京民工,而是因为我是与会人员中唯一的不和谐因素——光棍汉。搞得我满头大汗,穷于应付,只有招架之力,无还手之功。等下次去的时候,可能就好多了,因为胖乎乎的小孩子总是最吸引眼球的,嘿嘿。

Get free blog up and running in minutes with Blogsome
Theme designed by Jay of onefinejay.com