这在comp.lang.lisp thread from 2013 中进行了讨论。一位用户 jathd 指出:
您观察到的行为来自宏扩展的工作方式
一般:在处理评估(或编译,或
宏扩展,或...),如果是宏调用,则将其替换为
相应的展开并从头开始处理
用新的形式。
所以你必须积极地为符号宏做一些特殊的事情,
与常规宏相反,它们不是“递归的”。
Pascal Constanza 提供了以下建议:
一个好的解决办法是让符号宏展开成正则
宏。
(macrolet ((regular-macro (...) ...))
(symbol-macrolet ((sym (regular-macro)))
...))
尽管 informatimago 指出这仍然表现出与原始行为相同的行为:
如果常规宏扩展包含在
macroexpandable 放置命名符号宏的符号。
“有没有办法在不进行完整代码遍历的情况下仅扩展符号一次?”的答案?不幸的是,似乎是“不”。但是,解决这个问题并不难。链接线程中的“解决方案”最终使用 gensyms 来避免问题。例如:
(let ((x 32)) ; just to provide a value for x
(let ((#1=#:genx x)) ; new variable with x's value
(symbol-macrolet ((x (values #1#))) ; expansion contains the new variable
(* x x)))) ; === (* (values #1#) (values #1#))
;=> 1024
虽然在宏扩展中写#1# 或类似的东西并不好玩。如果您自动生成扩展,这还不错,但是如果您手动执行此操作,那么利用let 可以影响symbol-macrolet 的事实可能会很有用。这意味着您可以将扩展包装在 let 中,以恢复您想要的绑定:
(let ((x 32))
(let ((#1=#:genx x))
(symbol-macrolet ((x (let ((x #1#)) ; boilerplate
(values x)))) ; you get to refer to `x` here
(* x x))))
;=> 1024
如果您发现自己经常这样做,可以将其封装在符号宏的“无阴影”版本中:
(defmacro unshadowing-symbol-macrolet (((var expansion)) &body body)
"This is like symbol-macrolet, except that var, which should have a binding
in the enclosing environment, has that same binding within the expansion of
the symbol macro. This implementation only handles one var and expansion;
extending to n-ary case is left as an exercise for the reader."
(let ((hidden-var (gensym (symbol-name var))))
`(let ((,hidden-var ,var))
(symbol-macrolet ((,var (let ((,var ,hidden-var))
,expansion)))
,@body))))
(let ((x 32))
(unshadowing-symbol-macrolet ((x (values x)))
(* x x)))
;=> 1024
当然,这只适用于已经具有词法绑定的变量。 Common Lisp 没有提供太多访问环境对象的方式,除了在宏扩展中传递它们。如果您的实现提供环境访问,您可以让 unshadowing-symbol-macrolet 检查每个 var 是否绑定在环境中,如果是则提供本地阴影,如果不是则不进行阴影。
注意事项
有趣的是,看看该线程中的原作者 Antsan 不得不说他们对宏扩展过程如何工作的期望:
我认为宏扩展是通过反复进行宏扩展来实现的
在源上直到到达一个固定点。这样 SYMBOL-MACROLET
如果它被宏删除,将自动非递归
扩张。
类似:
(symbol-macrolet (a (foo a))
a)
macroexpand-1> (foo a)
macroexpand-1> (foo a) ; fixpoint
那里不需要特殊情况,尽管我猜这
宏扩展的算法会更慢。
这很有趣,因为这正是 Common Lisp 的 编译器 宏的工作方式。 define-compiler-macro 的文档说:
- 与普通宏不同,编译器宏可以仅通过返回与原始格式相同的形式来拒绝提供扩展
(可以使用 &whole 获得)。
这在这里并没有真正的帮助,因为符号宏无法选择返回什么;也就是说,没有参数传递给符号宏,因此没有任何东西可以检查或用于影响宏扩展是什么。返回相同表单的唯一方法是(symbol-macrolet ((x x)) …),这完全违背了目的。