Skip to content

LISP - 宏

宏是 Common Lisp 最强大和最具代表性的特性之一。它们允许你扩展语言自身的语法。俗话说,宏是生成代码的代码。

与在运行时操作值的函数不同,宏是一个转换函数,它接收 Lisp 代码(S-表达式)作为输入,并在编译时生成新的 Lisp 代码作为输出。然后,这段新代码将被编译并执行,以替代原始的宏调用。

你可以使用 defmacro 特殊操作符来定义宏。其语法与 defun 类似:

(defmacro macro-name (parameter-list)
"Optional documentation string."
body-of-macro)

宏的主体必须返回一个 Lisp 形式(一段代码),该形式将替换宏调用。

让我们创建一个 with-logging 宏,它在执行代码块前后打印一条消息。这是函数无法实现的功能,而且无需改变代码的调用方式。

要在宏内部构建代码,我们使用反引号 ( ) 语法。它就像一个模板。我们使用逗号 (,) 对值进行求值并将其插入到这个模板中,使用逗号-at (,@) 来拼接一个值列表。

(defmacro with-logging ((&key (stream '*standard-output*))
&body body)
"Executes BODY, printing a log message before and after."
`(progn
(format ,stream "--- Entering logged section ---")
,@body
(format ,stream "--- Exiting logged section ---")))
;; 现在,让我们使用新语法
(with-logging ()
(print "Doing some work...")
(let ((x 10) (y 20))
(format t "The sum is ~a~%" (+ x y))))

当你执行这段代码时,Lisp 会首先将宏展开为 progn 形式,然后运行生成的代码:

--- Entering logged section ---
"Doing some work..."
The sum is 30
--- Exiting logged section ---

参数列表中的 &body 关键字将所有后续形式收集到一个名为 body 的列表中。然后,宏主体构建一个 progn 块(该块按顺序求值表达式)。,@body 将 body 变量中的形式列表直接拼接(splice)到 progn 块中。

Common Lisp 有 loop,但让我们定义自己的 while 循环,以展示宏如何创建新的控制流结构。

(defmacro while (test &body body)
"Executes BODY as long as TEST evaluates to true."
`(loop while ,test
do (progn ,@body)))
;; 使用我们新的 `while` 循环
(let ((counter 3))
(while (> counter 0)
(format t "Counter is ~a~%" counter)
(decf counter)))

这段代码会展开成一个标准的 loop 宏调用,并产生预期结果:

Counter is 3
Counter is 2
Counter is 1
  • 创建新语法或控制流结构:如 with-logging 和 while 所示。
  • 控制求值:当你需要阻止参数被求值,或控制它们被求值的次数时(例如,短路逻辑运算符如 and 和 or)。
  • 在编译时减少样板代码:编写生成重复但略有不同的定义的代码。
  • 指导原则:如果你可以用普通函数解决问题,就使用函数。只有当函数不足以解决问题时,才考虑使用宏。