在 Peter Seibel 的《Practical Common Lisp》一书中,我们可以找到非常复杂的一次性宏的定义(参见页面底部http://www.gigamonkeys.com/book/macros-defining-your-own.html)。
我在过去 3 周内第 10 次阅读此宏定义,但无法理解它是如何工作的。:( 更糟糕的是,我无法自行开发此宏,即使我了解它的用途和使用方法。
我对逐步系统地“推导”这个臭名昭著的硬宏特别感兴趣!有什么帮助吗?
在 Peter Seibel 的《Practical Common Lisp》一书中,我们可以找到非常复杂的一次性宏的定义(参见页面底部http://www.gigamonkeys.com/book/macros-defining-your-own.html)。
我在过去 3 周内第 10 次阅读此宏定义,但无法理解它是如何工作的。:( 更糟糕的是,我无法自行开发此宏,即使我了解它的用途和使用方法。
我对逐步系统地“推导”这个臭名昭著的硬宏特别感兴趣!有什么帮助吗?
你在看这个吗:
(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 编码人员来说也很容易混淆。
这是宏用于编写其扩展的宏:一个宏,它编写宏的部分主体。
宏本身有一个plain let
,然后生成一次反引号let
,它将存在于使用once-only
. 最后,在用户使用宏的代码站点中,该宏的let
宏扩展中会出现一个双引号。
两轮生成 gensym 是必要的,因为once-only
它本身就是一个宏,所以它本身必须是卫生的;所以它会在最外层为自己生成一堆 gensyms let
。而且,目的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
不能是一个函数,因为它的工作是代表它的老板发明变量,使用它的宏,一个函数不能将变量引入它的调用者。
在Let Over Lambdaonce-only
中派生了 Practical Common Lisp 中宏的替代方案 (参见第三章中的“仅一次”部分)。
卡兹对它进行了精美而广泛的解释。
但是,如果您不太关心双重卫生问题,您可能会发现这个更容易理解:
(defmacro once-only ((&rest symbols) &body body)
;; copy-symbol may reuse the original symbol name
(let ((uninterned-symbols (mapcar 'copy-symbol symbols)))
;; For the final macro expansion:
;; Evaluate the forms in the original bound symbols into fresh bindings
``(let (,,@(mapcar #'(lambda (uninterned-symbol symbol)
``(,',uninterned-symbol ,,symbol))
uninterned-symbols symbols))
;; For the macro that is using us:
;; Bind the original symbols to the fresh symbols
,(let (,@(mapcar #'(lambda (symbol uninterned-symbol)
`(,symbol ',uninterned-symbol))
symbols uninterned-symbols))
,@body))))
第一个let
被反向引用了两次,因为它将成为最终扩展的一部分。目的是将原始绑定符号中的形式评估为新绑定。
第二个let
被反引号一次,因为它将成为once-only
. 目的是将原始符号重新绑定到新符号,因为它们的形式将在最终扩展中被评估并绑定到它们。
如果原始符号的重新绑定是在最终的宏扩展之前,那么最终的宏扩展将指代未插入的符号而不是原始形式。
with-slots
该用途的实现once-only
是一个需要双重卫生的示例:
(defmacro with-slots ((&rest slots) obj &body body)
(once-only (obj)
`(symbol-macrolet (,@(mapcar #'(lambda (slot)
`(,slot (slot-value ,obj ',slot)))
slots))
,@body)))
;;; Interaction in a REPL
> (let ((*gensym-counter* 1)
(*print-circle* t)
(*print-level* 10))
(pprint (macroexpand `(with-slots (a) (make-object-1)
,(macroexpand `(with-slots (b) (make-object-2)
body))))))
;;; With the double-hygienic once-only
(let ((#1=#:g2 (make-object-1)))
(symbol-macrolet ((a (slot-value #1# 'a)))
(let ((#2=#:g1 (make-object-2)))
(symbol-macrolet ((b (slot-value #2# 'b)))
body))))
;;; With this version of once-only
(let ((#1=#:obj (make-object-1)))
(symbol-macrolet ((a (slot-value #1# 'a)))
(let ((#1# (make-object-2)))
(symbol-macrolet ((b (slot-value #1# 'b)))
body))))
第二个扩展表明 inner正在遮蔽与 outterlet
变量的绑定。因此,在内部访问实际上会访问第二个对象。#:obj
let
a
with-slots
请注意,在此示例中,外部宏扩展获取名为 gensymg2
的内部g1
. 在正常的评估或编译中,情况正好相反,因为表格是从外部走到内部的。