在peter seibel的《实用的common lisp》一书中,我们只能找到非常复杂的宏的定义一次(参见http://www.gigamonkeys.com/book/macros-defining-your-own.html页的底部)。
在过去的三周里,我已经读了第十遍这个宏定义,但我不明白它是如何工作的:(更糟的是,我不能自己开发这个宏,即使我了解它的用途和如何使用它。
我特别感兴趣的是系统的“派生”这个臭名昭著的硬宏,一步一步有什么帮助吗?
最佳答案
你在看这个吗:
(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
。这两轮生成gensym是必要的,因为
once-only
本身是一个宏,因此它必须为自己的健康着想;因此它在最外面的let
中为自己生成一组gensym。但同时,once-only
的目的是简化另一个卫生宏的编写。因此它也为宏生成gensym。简而言之,
once-only
需要创建一个宏扩展,它需要一些值为gensyms的局部变量。这些局部变量将用于将gensyms插入另一个宏扩展中,以使其更卫生。这些局部变量本身必须是卫生的,因为它们是一个宏观展开,所以它们也是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
意外地捕获了count-form
或forms
内的引用,因为forms
只是数据,它将进入一段代码,最终插入到一个远程词汇范围(使用宏的站点)。我们需要担心的是,宏中的另一个变量不会混淆counter-sym
。例如,我们不能给局部变量命名count-form
。为什么?因为该名称是我们的函数参数之一;我们会隐藏它,从而产生编程错误。如果你想让一个宏来帮助你写这个宏,那么机器必须和你做同样的工作当它编写代码时,它必须发明一个变量名,并且必须注意它发明的名称。
但是,与您不同的是,代码编写机器看不到周围的作用域。它不能简单地查看存在哪些变量并选择不冲突的变量机器只是一个函数,它接受一些参数(未赋值的代码片段)并生成一段代码,然后在机器完成其工作后盲目地替换到一个作用域中。
因此,机器必须格外明智地选择名字。事实上,要完全防弹,它必须偏执和使用完全独特的符号:氏族。
所以继续这个例子,假设我们有一个机器人,它将为我们编写这个宏体。那个机器人可以是一个宏,
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
不能是一个函数,因为它的工作是代表它的老板(使用它的宏)发明变量,而函数不能将变量引入调用方。关于macros - 了解如何实现一次性Lisp宏,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/9808928/