Macros Lisp:宏可以是递归的吗?
我最近开始用Lisp编写代码,对宏印象最深——它们是在编译时编写的,这是我所知道的任何其他语言都无法做到的(即,在保持原始结构的情况下生成代码) 关于优化:我在同一代码中添加了类型注释(很多“fixnum”)。一旦我添加了3或4个宏,我意识到我做错了-这就是宏的用途,不要重复你自己Macros Lisp:宏可以是递归的吗?,macros,lisp,common-lisp,Macros,Lisp,Common Lisp,我最近开始用Lisp编写代码,对宏印象最深——它们是在编译时编写的,这是我所知道的任何其他语言都无法做到的(即,在保持原始结构的情况下生成代码) 关于优化:我在同一代码中添加了类型注释(很多“fixnum”)。一旦我添加了3或4个宏,我意识到我做错了-这就是宏的用途,不要重复你自己 ; whenever we want to indicate that the result of an operation ; fits in a fixnum, we macro expand (the fix
; whenever we want to indicate that the result of an operation
; fits in a fixnum, we macro expand (the fixnum (...))
(defmacro fast (&rest args)
`(the fixnum ,args))
...
(cond
(...)
(t (let* ((forOrange (+ (aref counts 5)
(fast * 2 (aref counts 6))
(fast * 5 (aref counts 7))
(fast * 10 (aref counts 8))))
(forYellow (+ (aref counts 3)
(fast * 2 (aref counts 2))
(fast * 5 (aref counts 1))
(fast * 10 (aref counts 0))))
…事实上,这是有效的:我没有在任何地方写很多“(fixnum(…)”),而是快速地在表达式前面加上“fast”——一切都很好
但是
我意识到,即使这样,事情也不应该停止:原则上,宏观“快速”应该。。。在这种情况下,将在评估的顶部调用:
(forYellow (fast + (aref counts 3)
(* 2 (aref counts 2))
(* 5 (aref counts 1))
(* 10 (aref counts 0))))
…并且它应该在所有子表达式中递归地“plant”“(fixnum(…))”
这能做到吗?
“defmacro”可以是递归的吗
更新:我在尝试这样做时遇到了一些非常奇怪的问题,因此我最终按照Rord的建议执行了以下操作-即实现了一个函数,在repl中测试了它,并从宏调用它:
(defun operation-p (x)
(or (equal x '+) (equal x '-) (equal x '*) (equal x '/)))
(defun clone (sexpr)
(cond
((listp sexpr)
(if (null sexpr)
()
(let ((hd (car sexpr))
(tl (cdr sexpr)))
(cond
((listp hd) (append (list (clone hd)) (clone tl)))
((operation-p hd) (list 'the 'fixnum (cons hd (clone tl))))
(t (cons hd (clone tl)))))))
(t sexpr)))
(defmacro fast (&rest sexpr)
`(,@(clone sexpr)))
它在SBCL下工作良好:
$ sbcl
This is SBCL 1.0.52, an implementation of ANSI Common Lisp.
...
* (load "score4.cl")
T
* (setf a '(+ (1 2) (- 1 (+ 5 6)))
...
* (clone a)
(THE FIXNUM (+ (1 2) (THE FIXNUM (- 1 (THE FIXNUM (+ 5 6))))))
* (macroexpand '(fast + 1 2 THE FIXNUM (- 1 THE FIXNUM (+ 5 6))))
(THE FIXNUM (+ 1 2 THE FIXNUM (THE FIXNUM (- 1 THE FIXNUM (THE FIXNUM (+ 5 6))))))
T
除了一个副作用外,一切正常:CMUCL可以工作,但不再编译代码:
; Error: (during macroexpansion)
; Error in KERNEL:%COERCE-TO-FUNCTION: the function CLONE is undefined.
哦,好吧:-)
更新:编译失败已在中解决。确定。您可以编写一个宏,以递归方式遍历参数表单,并以您想要的方式对其进行转换,一次一个子表单。因为这比听起来稍微复杂一些,所以正确的方法是使用库进行代码遍历
包含
hu.dwim.walker
,这显然是code walker的改进版本。宏不仅被调用,而且在使用时被扩展,因此在其自身定义中引用宏可能会变得混乱。但您不必这样做:宏可以调用常规函数,因此您可以编写一个常规函数来执行递归列表处理,然后从宏中使用它。您肯定可以编写一个在自身扩展中使用自身的宏。这很有意义,而且编写COND宏是一种自然的方式,由于任意长的cond对列表,它具有规则的扩展结构,可以用通常的car/cdr或first/rest递归来表示
(defmacro new-cond (&rest cond-pairs)
(cond ;; you need cond to compile new-cond syntax, LOL!
((null cond-pairs) nil)
((atom cond-pairs) (error "new-cond: bad syntax!"))
(t `(if ,(first (first cond-pairs))
(progn ,@(rest (first cond-pairs)))
(new-cond ,@(rest cond-pairs))))))
> (macroexpand-1 '(new-cond (1 2) (3 4)))
(IF 1 (PROGN 2) (NEW-COND (3 4)))
这与惰性列表处理非常类似macroexpand
仅扩展外部宏,留下一个“promise”对象继续扩展。“promise”对象是尾部的宏调用(NEW-COND(34))
当然,真正的宏扩展器将遍历整个表单并“强制”所有这些承诺(未扩展的宏调用),直到不再存在
我认为这种风格有一些优点,例如使用macroexpand
可以轻松调试。你得到一个小的扩展。如果它的递归性质是显而易见的,那就是胜利。如果宏是这样的,你的大脑需要看到整个事情扩大,这是一个损失
在这里,我可以看到(如果1(progn2)(NEW-COND(34))
是正确的编译。如果1为真,则评估表单列表(2)。否则,继续使用其他条件对。现在我们必须验证一对情况:
> (macroexpand-1 '(new-cond (3 4)))
(IF 3 (PROGN 4) (NEW-COND))
非常好,通过明显的检查,无对盒<代码>(NEW-COND),减少到零
其次,宏也可以在自己的代码中调用自身。因此,例如,如果我们是试图定义cond
的Lisp实现者,有一种方法我们实际上可以逃脱:
(defmacro cond (&rest cond-pairs)
(cond ;; you need cond to compile new-cond syntax, LOL!
((null cond-pairs) nil)
((atom cond-pairs) (error "new-cond: bad syntax!"))
(t `(if ,(first (first cond-pairs))
(progn ,@(rest (first cond-pairs)))
(new-cond ,@(rest cond-pairs))))))
如何在定义cond的宏中使用cond
?答案是自举。在宏中展开的cond
是一个现有的cond
,我们不知何故已经拥有了它。此新定义与现有的cond
扩展得很好,然后将其替换
这个过程可以重复。我们可以反复评估上述defmacro
表单;先前评估刚刚安装的cond
用于扩展新评估中出现的cond
如果我们用一个现有的Lisp编译器对一个Lisp编译器进行boostapping,我们可能可以使用宿主Lisp的cond
启动此过程,因此我们永远不需要第二个不依赖cond
的cond
宏的boostapping版本
请注意,要使其正常工作,我们的替换
cond
宏的主体必须在作为cond
的新定义安装之前完全展开。如果宏定义的主体未展开,则此操作无法工作;然后它就变成了一个递归调用。换句话说,这不是递归。递归无法工作;宏不需要自身来展开自身。充其量,它可能需要一个现有的宏,该宏不是它本身,但恰好具有相同的名称。风格建议:不要将要转换的表单拼接到宏调用中。将其作为一个子表单保存,即与(fast*…)
相比,更喜欢(fast*…)
。它使语义更简单(以案例(fast 3)
-这个表单应该是什么意思?)并且迎合了读者对表单结构的期望。@Matthias:最初我是这样做的-但我意识到在这种情况下有明确的语义(即“(fast 3)”只能表示“3”)和。。。这使它更易于使用:您可以在代码中看到要强制转换为fixnum的位置,您不必搜索paren匹配:只需在前面键入“fast”。这在Scheme中很常见。@leppie Right。它甚至有内置的先例,比如map
:(map-bling-xs)
。但请注意,对于CL,情况并非如此,在CL中,您必须编写(mapcar#bling xs)
。造成这种差异的原因是Scheme是一个Lisp-1,因此不存在关于什么的混淆