Skip to content

LISP - 作用域与绑定

Lisp - 宏卫生学:作用域(Scope)与绑定(Bindings)

Section titled “Lisp - 宏卫生学:作用域(Scope)与绑定(Bindings)”

宏(Macros)是强大的代码转换工具,但这种能力伴随着一项关键责任:管理变量作用域(variable scope)。由于宏直接将代码注入其调用点(call site),它们可能会意外地干扰现有变量。本章解释了变量捕获(variable capture)的问题,以及如何在 Common Lisp 中编写干净、可预测的卫生宏(hygienic macros)。

问题:不卫生宏(Unhygienic Macros)与变量捕获(Variable Capture)

Section titled “问题:不卫生宏(Unhygienic Macros)与变量捕获(Variable Capture)”

如果一个宏引入的绑定(bindings)可能意外地捕获或与周围代码中的变量冲突,那么它就是不卫生的(unhygienic)。当宏内部使用的符号与调用点处的符号相同时,就会发生这种情况。

让我们定义一个宏来交换两个位置的值。一个朴素的实现可能会使用一个名为 temp 的临时变量。

;; 有缺陷的宏:为其内部变量使用固定的符号 `temp`。
(defmacro unhygienic-swap (a b)
`(let ((temp ,a))
(setf ,a ,b)
(setf ,b temp)))

在大多数情况下,这工作得很好。然而,如果我们要交换的其中一个变量也叫 temp,它就会出现惊人的失败。

(let ((temp 10)
(my-var 20))
(format t "Before: temp=~d, my-var=~d~%" temp my-var)
(unhygienic-swap temp my-var)
(format t "After: temp=~d, my-var=~d~%" temp my-var))
Before: temp=10, my-var=20
After: temp=20, my-var=20

交换失败了!要了解原因,我们可以使用 macroexpand-1 来查看生成的代码:

;; CL-USER> (macroexpand-1 '(unhygienic-swap temp my-var))
(LET ((TEMP TEMP))
(SETF TEMP MY-VAR)
(SETF MY-VAR TEMP))

(let ((temp temp)) 绑定遮蔽了外部的 temp,导致 my-var 被设置为其自身的新值,而不是 temp 的原始值。

解决方案:使用 gensym 实现卫生宏

Section titled “解决方案:使用 gensym 实现卫生宏”

解决方案是使用保证唯一的符号。gensym 函数在每次调用时都会创建一个新的、未入池符号(uninterned symbol),它不会与任何现有符号冲突。

示例:一个正确、卫生的 swap 宏

Section titled “示例:一个正确、卫生的 swap 宏”
;; 正确的宏:为其内部变量使用生成的符号。
(defmacro hygienic-swap (a b)
(let ((temp-sym (gensym)))
`(let ((,temp-sym ,a))
(setf ,a ,b)
(setf ,b ,temp-sym))))

现在,我们再次尝试相同的测试用例:

(let ((temp 10)
(my-var 20))
(format t "Before: temp=~d, my-var=~d~%" temp my-var)
(hygienic-swap temp my-var)
(format t "After: temp=~d, my-var=~d~%" temp my-var))
Before: temp=10, my-var=20
After: temp=20, my-var=10

它正常工作了!现在宏展开使用了唯一的符号(例如 #:G1234),它不会与我们的 temp 变量冲突。

macrolet 特殊操作符(special operator)允许你定义只在特定词法作用域(lexical scope)内可用的局部宏。这对于在函数内部创建语法糖(syntactic sugar)或小型领域特定语言(DSL)非常有用,而不会污染全局命名空间。

(defun calculate-trajectory (x)
;; 定义一个只在该函数内部可用的局部宏 'square'。
(macrolet ((square (n)
`(* ,n ,n)))
;; 使用局部宏。
(+ (square x) (square (+ x 1)))))
(print (calculate-trajectory 5))
61
  • 始终为宏展开中引入的任何临时变量使用 gensym。
  • 避免对宏参数进行多次求值。如果参数可能有副作用(例如 (incf i)),请将其结果存储在一个由 gensym 生成的变量中,并在宏展开中使用该变量。