17

在 Peter Seibel 的《Practical Common Lisp》一书中,我们可以找到非常复杂的一次性宏的定义(参见页面底部http://www.gigamonkeys.com/book/macros-defining-your-own.html)。

我在过去 3 周内第 10 次阅读此宏定义,但无法理解它是如何工作的。:( 更糟糕的是,我无法自行开发此宏,即使我了解它的用途和使用方法。

我对逐步系统地“推导”这个臭名昭著的硬宏特别感兴趣!有什么帮助吗?

4

3 回答 3

25

你在看这个吗:

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

于 2012-03-21T18:28:05.910 回答
7

在Let Over Lambdaonce-only中派生了 Practical Common Lisp 中宏的替代方案 (参见第三章中的“仅一次”部分)。

于 2012-03-21T19:15:17.120 回答
1

卡兹对它进行了精美而广泛的解释。

但是,如果您不太关心双重卫生问题,您可能会发现这个更容易理解:

(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变量的绑定。因此,在内部访问实际上会访问第二个对象。#:objletawith-slots

请注意,在此示例中,外部宏扩展获取名为 gensymg2的内部g1. 在正常的评估或编译中,情况正好相反,因为表格是从外部走到内部的。

于 2012-04-15T15:13:37.423 回答