了解如何实现一次性lisp宏

时间:2012-03-21 16:36:37

标签: macros lisp common-lisp practical-common-lisp

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

我在过去3周内第10次阅读此宏定义,无法理解它是如何工作的。 :(更糟糕的是,我无法自己开发这个宏,即使我了解它的目的以及如何使用它。

我特别感兴趣的是系统的"推导"这个臭名昭着的宏观,一步一个脚印!有什么帮助吗?

3 个答案:

答案 0 :(得分: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程序员也容易混淆。

这是宏用于编写扩展的宏:一个写宏部分主体的宏。

在宏本身中有一个普通的let,然后是一次反引号生成的let,它将存在于使用once-only的宏体内。最后,在用户使用宏的代码网站中,会出现在那个宏的宏扩展中出现的双重反引号let

两轮生成的gensyms是必要的,因为once-only本身就是一个宏,因此它必须是卫生的;所以它在最外面的let中为自己生成了一堆gensyms。而且,once-only的目的是简化另一个卫生宏的写作。因此它也会为该宏生成gensyms。

简而言之,once-only需要创建一个宏扩展,它需要一些值为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-formforms中的引用,因为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 不能是一个函数,因为它的作业是代表其老板发明变量,使用它的宏,而函数不能将变量引入其调用者。

答案 1 :(得分:7)

来自Practical Common Lisp的once-only宏的替代版本在Let Over Lambda中得出(参见第三章中的“仅限一次”部分)。

答案 2 :(得分:2)

Kaz非常精彩地解释了它。

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

(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的{​​{1}}实施是一个需要双重卫生的例子:

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)))) 正在影响与外部let的变量#:obj的绑定。因此,访问内部let内的a实际上将访问第二个对象。

请注意,在此示例中,outter宏扩展获得名为with-slots的gensym和内部g2。在正常的评价或编纂中,情况正好相反,因为形式从外在走向内在。