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)。当宏内部使用的符号与调用点处的符号相同时,就会发生这种情况。
示例:一个有缺陷的 swap 宏
Section titled “示例:一个有缺陷的 swap 宏”让我们定义一个宏来交换两个位置的值。一个朴素的实现可能会使用一个名为 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=20After: 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=20After: temp=20, my-var=10它正常工作了!现在宏展开使用了唯一的符号(例如 #:G1234),它不会与我们的 temp 变量冲突。
使用 macrolet 定义局部宏
Section titled “使用 macrolet 定义局部宏”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编写宏的最佳实践
Section titled “编写宏的最佳实践”- 始终为宏展开中引入的任何临时变量使用
gensym。 - 避免对宏参数进行多次求值。如果参数可能有副作用(例如
(incf i)),请将其结果存储在一个由gensym生成的变量中,并在宏展开中使用该变量。