Skip to content

LISP - 宏编写风格

宏是元编程的强大工具,但其强大功能也伴随着相应的责任。编写清晰、可预测且易于维护的宏对于健康的代码库至关重要。本教程涵盖了专业宏开发的基本最佳实践。

卫生性(Hygiene)是宏编写中最关键的概念。一个不卫生的宏会引入内部变量,这些变量可能意外地与使用宏的代码中的变量发生冲突。这被称为变量捕获(variable capture),会导致不可预测的 bug。

gensym 函数生成一个全新的、唯一的符号,保证不会在代码的任何其他地方存在。通过在宏内部为所有内部变量使用 gensym,可以确保永远不会发生名称冲突。

;; 一个用于交换两个位置(变量)值的宏。
(defmacro swap (x y)
"Swaps the values of two places X and Y."
(let ((temp-var (gensym "TEMP"))) ; 为临时变量创建一个唯一符号。
`(let ((,temp-var ,x))
(setf ,x ,y)
(setf ,y ,temp-var))))
;; --- 用法 ---
(let ((a 5) (b 10))
(format t "Before: a=~a, b=~a~%" a b)
(swap a b)
(format t "After: a=~a, b=~a~%" a b))
;; 即使用户定义了一个名为 TEMP 的变量,这也有效。
(let ((a 1) (temp 99))
(format t "Before: a=~a, temp=~a~%" a temp)
(swap a temp)
(format t "After: a=~a, temp=~a~%" a temp))

通过手动构建列表(例如 (list 'let ...))来构建代码是冗长且容易出错的。准引用(Quasiquotation)(反引号语法)是宏内部构建代码模板的标准且最易读的方式。

  • 反引号 ( ):创建一个代码模板。其中大部分被视为字面数据。
  • 逗号 (,):“取消引用”后面的表达式,对其求值并将结果插入到模板中。
  • 逗号-at (,@):“拼接”后面的表达式。它必须求值为一个列表,并且列表的元素会被插入到模板中,就好像它们是直接在那里键入的一样。

当宏行为异常时,你最有价值的工具是 macroexpand-1。这个函数接受一个宏形式,并向你展示它展开后的代码,深度为一层。这允许你检查生成的代码并发现错误。

;; 在 REPL 中,引用你想要检查的形式。
CL-USER> (macroexpand-1 '(swap my-var other-var))
;; 输出将类似于这样:
(LET ((#:TEMP1057 MY-VAR))
(SETF MY-VAR OTHER-VAR)
(SETF OTHER-VAR #:TEMP1057))
T

#:TEMP1057 是由 gensym 创建的未注册符号(uninterned symbol)的打印表示。看到这个证实了我们的宏是卫生的。

一个简单的宏可能会对其参数求值多次,如果参数有副作用,这会导致不正确的行为。

;; 糟糕的宏:`x` 被求值两次!
(defmacro bad-square (x)
`(* ,x ,x))
(let ((i 2))
(bad-square (incf i))) ; 展开为 (* (incf i) (incf i)),结果是 3 * 4 = 12!
;; 好的宏:对参数求值一次并存储在临时变量中。
(defmacro good-square (x)
(let ((val (gensym)))
`(let ((,val ,x))
(* ,val ,val))))
(let ((i 2))
(good-square (incf i))) ; 正确求值为 3 * 3 = 9

始终在宏中包含文档字符串。解释:

  • 宏的作用。
  • 它引入的语法。
  • 任何不明显的求值规则。
  • 清晰的使用示例。