在Peter Seibel的书" Practical Common Lisp"中,我们可以找到非常复杂的宏的定义(参见页面底部http://www.gigamonkeys.com/book/macros-defining-your-own.html)。
我在过去3周内第10次阅读此宏定义,无法理解它是如何工作的。 :(更糟糕的是,我无法自己开发这个宏,即使我了解它的目的以及如何使用它。
我特别感兴趣的是系统的"推导"这个臭名昭着的宏观,一步一个脚印!有什么帮助吗?
答案 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-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
不能是一个函数,因为它的作业是代表其老板发明变量,使用它的宏,而函数不能将变量引入其调用者。
答案 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
。在正常的评价或编纂中,情况正好相反,因为形式从外在走向内在。