Skip to content

LISP - 使用宏生成代码

在 Lisp 中,宏是元编程(即编写生成代码的代码)的强大工具。与在执行前评估其参数的函数不同,宏接收的是其参数未评估的 Lisp 代码。这使得您可以在代码编译和运行之前对其进行转换,从而有效地用新的语法和控制结构扩展 Lisp 语言。在本章中,我们将探讨如何使用宏构建我们自己的语言特性。

理解宏的关键在于,它们操作的是代码,而函数操作的是数据(评估后的代码结果)。让我们看看这为什么很重要。

考虑一个 square 函数:

(defun square-fn (n) (* n n))

如果您用 (square-fn (+ 2 3)) 调用它,(+ 2 3) 会首先被评估,变为 5,然后调用 (square-fn 5)。这通常是您想要的行为。

但如果参数有副作用呢?

(let ((x 1))
(square-fn (incf x))) ; incf 递增 x 并返回新值

这里,(incf x) 只运行一次,x 变为 2,然后调用 (square-fn 2),返回 4。副作用只发生一次。宏的行为则不同,如果编写不小心,这可能导致意想不到的行为。

让我们定义一个宏来生成一个数的平方代码。我们使用 defmacro 和反引号 ( `) 语法进行模板化。

;; 一个生成表达式平方代码的宏。
(defmacro my-square (expr)
"展开为将表达式乘以自身的代码。"
`(* ,expr ,expr))
;; --- 用法 ---
;; 使用内置的 macroexpand-1 查看宏生成了什么。
(print (macroexpand-1 '(my-square 5)))
(terpri)
;; 正常执行
(print (my-square 5))
(terpri)
;; 带副作用会发生什么?
(let ((counter 0))
(print (my-square (incf counter))) ; 展开为 (* (incf counter) (incf counter))
(print counter)) ; 计数器被递增了两次!
(* 5 5)
T
25
4
2
  • defmacro my-square (expr):定义了一个名为 my-square 的宏,它接受一个参数 expr。重要的是,expr 是传递给宏的未评估的代码,而不是它的值。
  • 反引号 ( `):反引号开始一个模板。里面的一切都被视为字面数据,而不是立即执行的代码。
  • 逗号 (,):逗号“取消引用”一个项,告诉 Lisp 评估它并将其值拼接到模板中。这里,,expr 插入了 5 或 (+ 2 3) 的代码树。incf 的双重评估陷阱是一个经典的宏问题。
  • macroexpand-1:这是一个关键的调试工具。它一步一步地向您展示宏生成的代码,让您在运行之前验证其输出。

示例:使用 Gensym 的卫生 square 宏

Section titled “示例:使用 Gensym 的卫生 square 宏”

为了解决多重评估问题,我们需要确保表达式只被评估一次。我们可以通过在宏展开内部创建一个临时变量来实现这一点。为了避免与周围代码中的变量意外冲突,我们必须使用 gensym 来创建一个唯一的、未驻留的符号。

;; 一个更好、更安全的平方宏。
(defmacro my-safe-square (expr)
"通过只评估一次表达式来安全地平方它。"
(let ((temp-var (gensym)))
`(let ((,temp-var ,expr))
(* ,temp-var ,temp-var))))
;; --- 用法 ---
(print (macroexpand-1 '(my-safe-square (incf counter))))
(terpri)
(let ((counter 0))
(print (my-safe-square (incf counter)))
(print counter)); 计数器现在只递增一次!
(LET ((#<GENSYM> (INCF COUNTER))) (* #<GENSYM> #<GENSYM>))
T
1
1
  • 卫生:这个宏被认为是“卫生”的,因为它避免了意外的变量捕获和重复评估。
  • gensym:生成一个新的、唯一的符号(例如 #<GENSYM>),它不可能与任何现有变量名冲突。
  • let:宏展开内部的 let 形式创建了一个临时变量。评估 expr 的结果存储在这个变量中,然后被使用两次。

在创建新的控制结构时,宏大放异彩。这是一个简单的 while 循环,一种 Common Lisp 中没有内置的结构。

;; 一个用于经典 'while' 循环的宏。
(defmacro my-while (condition &body body)
"只要条件为真,就执行主体表达式。"
`(loop while ,condition
do (progn
,@body)))
;; --- 用法 ---
(let ((i 0))
(my-while (< i 5)
(print i)
(incf i)))
0
1
2
3
4
  • &body:这是一个特殊的 lambda-list 关键字,它将所有剩余形式收集到一个列表中,非常适合用作循环或函数式结构的主体。
  • 逗号-at (,@):逗号-at 运算符将一个形式列表拼接(splice)到模板中。如果 body 是 ((print i) (incf i)),那么 ,@body 会将 (print i) (incf i) 直接插入到 progn 形式中。progn 用于将多个表达式分组到一个块中。
  • 何时使用宏?:仅当函数无法满足需求时。如果您需要控制评估、创建新语法或执行编译时计算,请使用宏。
  • 保持简单:复杂的宏难以调试。如果宏变得过于复杂,请考虑在宏定义内部使用辅助函数。
  • 始终使用 gensym:对于宏展开中的任何临时变量,请使用 gensym 以避免错误。
  • 使用 macroexpand-1 调试:始终检查宏生成的代码。这是发现错误最有效的方法。