你在看这个吗:
(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)))))
它并没有那么复杂,但它确实有一个嵌套的反引号,并且有多个相互相似的层级,即使对于有经验的 Lisp 编码人员来说也很容易混淆。
这是一个宏,用于编写宏的扩展:一个宏,用于编写宏的部分主体。
在宏本身有一个普通的let,然后是一次反引号生成的let,它将存在于使用once-only的宏的主体内。最后,有一个双重反引号let,它将出现在that宏的宏扩展中,在用户使用宏的代码站点中。
这两轮生成gensyms是必要的,因为once-only本身就是一个宏,所以为了它自己必须要卫生;所以它会在最外面的let 中为自己生成一堆 gensyms。而且,once-only 的目的是简化另一个卫生宏的编写。所以它也会为那个宏生成 gensyms。
简而言之,once-only 需要创建一个宏扩展,它需要一些值为 gensyms 的局部变量。这些局部变量将用于将 gensyms 插入另一个宏扩展中以使其卫生。而且这些局部变量本身必须是卫生的,因为它们是宏扩展,所以它们也是 gensyms。
如果你正在编写一个普通的宏,你有保存 gensyms 的局部变量,例如:
;; silly example
(defmacro repeat-times (count-form &body forms)
(let ((counter-sym (gensym)))
`(loop for ,counter-sym below ,count-form do ,@forms)))
在编写宏的过程中,您发明了一个符号counter-sym。这个变量是在普通视图中定义的。你,人类,以这样一种方式选择了它,它不会与词汇范围内的任何东西发生冲突。有问题的词法范围是您的宏的词法范围。我们不必担心counter-sym 会意外捕获count-form 或forms 中的引用,因为forms 只是进入一段代码的数据,该代码最终将插入到某个远程词法范围(网站使用宏的地方)。我们必须担心不要将counter-sym 与宏中的另一个变量混淆。例如,我们不能给局部变量命名为count-form。为什么?因为该名称是我们的函数参数之一;我们会隐藏它,造成编程错误。
现在,如果您想要一个宏来帮助您编写该宏,那么机器必须完成与您相同的工作。在写代码的时候,它必须要发明一个变量名,而且要小心它发明的名字。
但是,与您不同,编写代码的机器看不到周围的范围。它不能简单地查看存在哪些变量并选择不冲突的变量。这台机器只是一个函数,它接受一些参数(一段未评估的代码)并生成一段代码,然后在该机器完成其工作后将其盲目地替换到一个作用域中。
因此,机器必须格外明智地选择名称。事实上,要完全防弹,它必须是偏执狂并使用完全独特的符号:gensyms。
继续这个例子,假设我们有一个机器人会为我们编写这个宏体。那个机器人可以是宏,repeat-times-writing-robot:
(defmacro repeat-times (count-form &body forms)
(repeat-times-writing-robot count-form forms)) ;; macro call
机器人宏可能是什么样的?
(defmacro repeat-times-writing-robot (count-form forms)
(let ((counter-sym-sym (gensym))) ;; robot's gensym
`(let ((,counter-sym-sym (gensym))) ;; the ultimate gensym for the loop
`(loop for ,,counter-sym-sym below ,,count-form do ,@,forms))))
您可以看到它如何具有once-only 的一些特性:双重嵌套和(gensym) 的两个层次。如果你能理解这一点,那么到once-only 的飞跃就很小了。
当然,如果我们只是想让机器人编写重复次数,我们可以把它做成一个函数,然后这个函数就不必担心发明变量:它不是宏,所以它不会需要卫生:
;; i.e. regular code refactoring: a piece of code is moved into a helper function
(defun repeat-times-writing-robot (count-form forms)
(let ((counter-sym (gensym)))
`(loop for ,counter-sym below ,count-form do ,@forms)))
;; ... and then called:
(defmacro repeat-times (count-form &body forms)
(repeat-times-writing-robot count-form forms)) ;; just a function now
但是once-only不能是一个函数,因为它的工作是代表它的老板发明变量,使用它的宏,一个函数不能引入变量进入它的调用者。