Macros 了解如何实现一次性lisp宏

Macros 了解如何实现一次性lisp宏,macros,lisp,common-lisp,practical-common-lisp,Macros,Lisp,Common Lisp,Practical Common Lisp,在Peter Seibel的《实用公共Lisp》一书中,我们可以只找到一次非常复杂的宏的定义(见页面底部) 在过去的三周里,我已经第十次阅读这个宏定义了,我无法理解它是如何工作的(更糟糕的是,我无法自行开发此宏,即使我了解其用途和使用方法 我特别感兴趣的是系统地一步一步地“派生”出这个众所周知的硬宏!有什么帮助吗?您正在看这个: (defmacro once-only ((&rest names) &body body) (let ((gensyms (loop for n

在Peter Seibel的《实用公共Lisp》一书中,我们可以只找到一次非常复杂的宏的定义(见页面底部)

在过去的三周里,我已经第十次阅读这个宏定义了,我无法理解它是如何工作的(更糟糕的是,我无法自行开发此宏,即使我了解其用途和使用方法


我特别感兴趣的是系统地一步一步地“派生”出这个众所周知的硬宏!有什么帮助吗?

您正在看这个:

(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
,它将位于只使用一次
的宏主体中。最后,在e该宏由用户使用

两轮生成gensym是必要的,因为
仅一次
本身就是一个宏,因此它本身必须是卫生的;因此它在最外层的
let
中为自己生成一组gensym。但是,
仅一次
的目的是简化另一个卫生宏的编写。因此它生成该宏的gensym也是

简而言之,
仅一次
需要创建一个宏扩展,该扩展需要一些值为gensyms的局部变量。这些局部变量将用于将gensyms插入另一个宏扩展中,以使其卫生。这些局部变量本身必须卫生,因为它们是宏扩展,因此它们是lso-gensyms

如果您正在编写一个普通宏,则会有一些包含gensym的局部变量,例如:

;; 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
acc相同地捕获
计数表单
表单
中的引用,因为
表单
只是进入一段代码的数据,最终将插入某个远程词法范围(使用宏的站点)。我们必须担心不会将
计数器sym
与宏中的另一个变量混淆。例如,我们不能将局部变量的名称命名为
count form
。为什么?因为该名称是我们的函数参数之一;我们会对其进行阴影处理,从而产生编程错误

现在,如果你想要一个宏来帮助你编写这个宏,那么机器必须和你做同样的工作。当它编写代码时,它必须发明一个变量名,并且必须小心它发明的名称

但是,与您不同的是,代码编写机器看不到周围的作用域。它不能简单地查看存在哪些变量并选择不冲突的变量。该机器只是一个接受一些参数(未计算代码片段)的函数并生成一段代码,然后在机器完成其工作后盲目地将其替换到范围中

因此,机器必须格外明智地选择名称。事实上,要完全防弹,它必须是偏执狂,并使用完全独特的符号:gensym

继续这个例子,假设我们有一个机器人,它将为我们编写这个宏体。这个机器人可以是一个宏,
重复时间编写机器人

(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))))
您可以看到它如何具有仅一次
的一些功能:双重嵌套和两级
(gensym)
。如果您能理解这一点,那么到仅一次
的飞跃很小

当然,如果我们只是想要一个机器人来写重复时间,我们会把它变成一个函数,然后这个函数就不必担心变量的产生:它不是一个宏,所以它不需要:

 ;; 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
不能成为函数,因为它的工作是代表它的老板(使用它的宏)发明变量,而函数不能将变量引入它的调用者。

从实用的公共Lisp派生出一种替代
once only
宏的方法(请参阅第三章中的“仅限一次”部分)。

卡兹对其进行了详尽而优美的解释

然而,如果你不太关心双重卫生问题,你可能会发现这一点更容易理解:

(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
被反引用一次,因为它将是
用户的一部分,只使用一次
。目的是将原始符号重新绑定到新符号,因为它们的形式将在最终扩展中进行评估并绑定到它们

如果原始符号的重新绑定在最终宏展开之前,则最终宏展开将引用未涉及的符号,而不是原始形式

带有插槽的
实现只使用
一次
,就是一个需要双重卫生的示例:

(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))))
第二个扩展显示内部
let
隐藏到外部
let
的变量
#:obj
的绑定。因此,使用插槽
w访问内部
中的
a