LISP - 使用宏生成代码
Lisp - 使用宏生成代码
Section titled “Lisp - 使用宏生成代码”在 Lisp 中,宏是元编程(即编写生成代码的代码)的强大工具。与在执行前评估其参数的函数不同,宏接收的是其参数未评估的 Lisp 代码。这使得您可以在代码编译和运行之前对其进行转换,从而有效地用新的语法和控制结构扩展 Lisp 语言。在本章中,我们将探讨如何使用宏构建我们自己的语言特性。
宏 vs. 函数:核心区别
Section titled “宏 vs. 函数:核心区别”理解宏的关键在于,它们操作的是代码,而函数操作的是数据(评估后的代码结果)。让我们看看这为什么很重要。
考虑一个 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。副作用只发生一次。宏的行为则不同,如果编写不小心,这可能导致意想不到的行为。
示例:一个简单的 square 宏
Section titled “示例:一个简单的 square 宏”让我们定义一个宏来生成一个数的平方代码。我们使用 defmacro 和反引号 ( `) 语法进行模板化。
main.lisp
Section titled “main.lisp”;; 一个生成表达式平方代码的宏。(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
42defmacro my-square (expr):定义了一个名为my-square的宏,它接受一个参数expr。重要的是,expr是传递给宏的未评估的代码,而不是它的值。- 反引号 (
`):反引号开始一个模板。里面的一切都被视为字面数据,而不是立即执行的代码。 - 逗号 (
,):逗号“取消引用”一个项,告诉 Lisp 评估它并将其值拼接到模板中。这里,,expr插入了5或(+ 2 3)的代码树。incf的双重评估陷阱是一个经典的宏问题。 macroexpand-1:这是一个关键的调试工具。它一步一步地向您展示宏生成的代码,让您在运行之前验证其输出。
示例:使用 Gensym 的卫生 square 宏
Section titled “示例:使用 Gensym 的卫生 square 宏”为了解决多重评估问题,我们需要确保表达式只被评估一次。我们可以通过在宏展开内部创建一个临时变量来实现这一点。为了避免与周围代码中的变量意外冲突,我们必须使用 gensym 来创建一个唯一的、未驻留的符号。
main.lisp
Section titled “main.lisp”;; 一个更好、更安全的平方宏。(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
11- 卫生:这个宏被认为是“卫生”的,因为它避免了意外的变量捕获和重复评估。
gensym:生成一个新的、唯一的符号(例如#<GENSYM>),它不可能与任何现有变量名冲突。let:宏展开内部的let形式创建了一个临时变量。评估expr的结果存储在这个变量中,然后被使用两次。
示例:创建一个自定义循环
Section titled “示例:创建一个自定义循环”在创建新的控制结构时,宏大放异彩。这是一个简单的 while 循环,一种 Common Lisp 中没有内置的结构。
main.lisp
Section titled “main.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)))01234&body:这是一个特殊的 lambda-list 关键字,它将所有剩余形式收集到一个列表中,非常适合用作循环或函数式结构的主体。- 逗号-at (
,@):逗号-at 运算符将一个形式列表拼接(splice)到模板中。如果body是((print i) (incf i)),那么,@body会将(print i) (incf i)直接插入到progn形式中。progn用于将多个表达式分组到一个块中。
最佳实践与调试
Section titled “最佳实践与调试”- 何时使用宏?:仅当函数无法满足需求时。如果您需要控制评估、创建新语法或执行编译时计算,请使用宏。
- 保持简单:复杂的宏难以调试。如果宏变得过于复杂,请考虑在宏定义内部使用辅助函数。
- 始终使用
gensym:对于宏展开中的任何临时变量,请使用gensym以避免错误。 - 使用
macroexpand-1调试:始终检查宏生成的代码。这是发现错误最有效的方法。