LISP - 变量捕获与卫生宏
Lisp - 变量捕获与卫生宏
Section titled “Lisp - 变量捕获与卫生宏”宏(Macros)是 Lisp 最强大的特性之一,它允许你编写能够生成代码的代码,从而有效地扩展语言本身。然而,这种强大性也伴随着一个隐秘的陷阱:变量捕获(variable capture)。当宏引入的变量无意中与使用该宏的代码中的变量发生冲突时,就会发生这种情况。易受此问题影响的宏被称为不卫生宏(unhygienic macro)。
本章将探讨什么是变量捕获、如何识别它,以及如何使用标准 Lisp 工具编写卫生宏(hygienic macro)——即能够避免这个问题的健壮宏。
问题:一个不卫生宏
Section titled “问题:一个不卫生宏”让我们通过一个经典(尽管有些刻意)的示例来演示变量捕获:一个用于交换两个变量值的宏。虽然内置的 rotatef 是完成这项工作的正确工具,但 swap 却能很好地说明这个问题。
main.lisp
Section titled “main.lisp”;; 一个用于交换两个位置值的非卫生宏。(defmacro unhygienic-swap (x y) `(let ((temp ,x)) ; 这个 'temp' 是问题根源。 (setf ,x ,y) (setf ,y temp)))
;; 让我们尝试使用名为 'a' 和 'b' 的变量。(let ((a 1) (b 2)) (unhygienic-swap a b) (format t "a: ~a, b: ~a~%" a b))
;; 现在,我们来看看当其中一个参数名为 'temp' 时会发生什么。(let ((temp 10) (my-var 20)) (format t "Before swap: temp=~a, my-var=~a~%" temp my-var) (unhygienic-swap temp my-var) ; 糟糕!问题就出在这里。 (format t "After swap: temp=~a, my-var=~a~%" temp my-var))Output
Section titled “Output”a: 2, b: 1Before swap: temp=10, my-var=20After swap: temp=20, my-var=20解释:哪里出了问题?
Section titled “解释:哪里出了问题?”第一次使用 a 和 b 进行的交换完美地工作了。问题出现在第二种情况。为了理解原因,我们需要看看宏展开后的代码。你可以在 Lisp 环境中使用 macroexpand-1 来实现这一点。
;; (unhygienic-swap temp my-var) 的展开形式如下:
(let ((temp temp)) ; <-- 捕获发生在这里! (setf temp my-var) (setf my-var temp))- 宏引入了它自己的局部变量,名为
temp。 - 调用宏的代码也使用了名为
temp的变量。 - 在展开后的代码中,
(let ((temp temp))...)创建了一个新的temp,它遮蔽了外部的temp。内部的setf调用只修改这个新的内部temp。 - 外部的
temp(初始值为 10)从未被赋值给my-var。结果是my-var被赋予了内部temp的新值,而这个新值也是由my-var设置的。实际上,my-var被赋值给了它自己,而外部temp的原始值丢失了。
解决方案:使用 gensym 实现卫生
Section titled “解决方案:使用 gensym 实现卫生”为了解决这个问题,我们必须确保宏创建的任何临时变量都具有唯一的名称,这些名称绝不会与用户代码冲突。Common Lisp 提供了 gensym 函数正是为了这个目的。gensym 每次调用都会生成一个新的、唯一的符号。
main.lisp
Section titled “main.lisp”;; 一个使用 GENSYM 创建唯一临时变量的卫生宏。(defmacro hygienic-swap (x y) (let ((temp-sym (gensym))) ; 1. 创建一个唯一的符号,例如 #:G1023 `(let ((,temp-sym ,x)) ; 2. 在生成的代码中使用该唯一符号。 (setf ,x ,y) (setf ,y ,temp-sym))))
(let ((temp 10) (my-var 20)) (format t "Before swap: temp=~a, my-var=~a~%" temp my-var) (hygienic-swap temp my-var) ; 现在它正常工作了! (format t "After swap: temp=~a, my-var=~a~%" temp my-var))Output
Section titled “Output”Before swap: temp=10, my-var=20After swap: temp=20, my-var=10解释:为何有效
Section titled “解释:为何有效”让我们看看卫生宏的展开形式:
;; (hygienic-swap temp my-var) 的展开形式大概会是这样:;; 注意:#:G1023 只是 GENSYM 可能创建的符号示例。
(let ((#:G1023 temp)) (setf temp my-var) (setf my-var #:G1023))(let ((temp-sym (gensym))) ... ):这部分在宏定义时运行。它创建一个新符号并存储在temp-sym变量中。- **
(let ((,temp-sym ,x)) … )**:在反引号模板内部,temp-sym被取消引用(unquoted)。这会将**唯一的符号本身**(例如#:G1023`)注入到宏生成的代码中。 - 无冲突:生成的代码现在使用一个名为
#:G1023的变量,它不可能与用户的变量temp发生冲突。交换逻辑现在按预期工作。
最佳实践和调试
Section titled “最佳实践和调试”- 尽可能使用内置函数:对于交换操作,正确、符合习惯且卫生(hygienic)的方法是使用
(rotatef place1 place2)。始终优先使用内置函数或宏,而不是自己编写。 - 宏的临时变量始终使用
gensym:宏内部引入的任何变量,如果不是其参数之一,都应该用gensym创建,以保证卫生性(hygiene)。 - 使用
macroexpand-1调试:调试宏最重要的工具是macroexpand-1。它能准确地显示你的宏正在生成什么样的代码,这几乎总能揭示问题根源。