Skip to content

LISP - 编译器宏

在 Common Lisp 中,宏(macros)是元编程(metaprogramming)的基础特性,允许您在代码编译前对其进行转换和生成。编译器宏(compiler macro)是该系统中的一种专用工具,专门为优化而设计。本章探讨了标准宏(defmacro)和编译器宏(define-compiler-macro)之间的关系和区别。

理解何时以及为何应用每种类型的宏是关键:

  • 标准宏(defmacro):它们的主要目的是语法扩展。它们允许您创建新的控制结构、领域特定语言(DSL)或仅仅减少样板代码(boilerplate code)。标准宏在编译器主工作开始之前,将一个 S-表达式转换为另一个 S-表达式。编译器只看到扩展后的代码。
  • 编译器宏(define-compiler-macro):它们的目的在于优化。它们为函数调用提供了一个替代的、更高效的实现。Lisp 编译器在其优化阶段调用编译器宏,为其提供一个更快代码序列的“建议”。编译器可以选择忽略此建议。

让我们看看它们是如何定义的以及在哪里使用。

标准宏使用 defmacro 定义。其参数是源代码中的原始 S-表达式,而不是它们求值后的结果。它返回一个新的 S-表达式,以在其位置进行编译和求值。

一个经典的例子是创建一个 unless 控制结构,它与 when 相反。

;; 定义一个新的语法构造 `unless`。
(defmacro unless (condition &body body)
"如果条件为假,则执行主体代码。"
`(when (not ,condition)
,@body))
;; 用法:
(let ((user-logged-in nil))
(unless user-logged-in
(print "Redirecting to login page...")))
;; 编译器看到的代码:
;; (when (not user-logged-in)
;; (print "Redirecting to login page..."))

编译器宏使用 define-compiler-macro 定义,并与特定函数关联。它将整个函数调用形式作为参数接收。它的目标是如果可能,返回一个更优化的形式。如果它无法优化该调用,它应该返回原始形式,以便编译器正常处理。

考虑一个获取序列长度的函数。我们可以提供一个编译器宏,以便在编译器能够证明参数是列表或向量时,使用更高效、特定于类型的函数。

;; 首先,定义常规函数。
(defun generic-length (seq)
(length seq))
;; 现在,为编译器定义一个优化提示。
(define-compiler-macro generic-length (&whole form arg)
"如果编译器知道 arg 的类型,则使用更快的函数。"
(if (and (listp arg) (= (length arg) 2) (eq (first arg) 'the))
;; 形式是 `(generic-length (the list ...))` 或 `(generic-length (the vector ...))`
(let ((type (second arg)))
(cond ((subtypep type 'list) `(list-length ,(third arg)))
((subtypep type 'vector) `(vector-length ,(third arg)))
(t form) ; 无法优化,返回原始形式
))
form ; 无法确定类型,返回原始形式
))
;; 在可获得类型信息的函数中:
(defun count-my-list-items (my-list)
(declare (type list my-list))
(generic-length my-list)) ; 编译器宏可能会将其展开为 (list-length my-list)
;; 注意:`list-length` 和 `vector-length` 是假设的快速函数。
;; 原理是用特定的、更快的调用替换泛型调用。
特性defmacro (标准宏)define-compiler-macro (编译器宏)
目的语法抽象、DSL、代码生成性能优化
运行时间编译之前(在宏展开阶段)编译期间(在优化阶段)
输入参数(未求值的 S-表达式)整个函数调用形式(例如,(my-func a b))
是函数吗?否。它定义的是代码转换,而不是可调用的函数。否。它是对现有可调用函数的转换提示。
编译器能否忽略?否。展开是强制性的。是。编译器可以忽略该提示并使用原始函数定义。
何时使用当您需要 Lisp 中不存在的新语法时。当一个函数对于特定参数类型或值有更高效的实现,并且这可以在编译时确定时。
  • 调试宏:在您的 REPL(Read-Eval-Print Loop,读取-求值-打印循环)中使用 macroexpand-1 和 macroexpand 来查看您的标准宏会展开成什么。这对于查找生成代码中的错误至关重要。
  • 谨慎使用:编译器宏是一个高级特性。除非您通过性能分析(profiling)确定了性能瓶颈,否则不要使用它们。过早优化可能导致代码复杂且难以调试。
  • 保持语义:编译器宏不得改变原始函数的行为。它应该是一种纯粹的优化。
  • 安全回退:始终确保当编译器宏无法应用优化时,返回原始形式。这能确保您的代码保持正确。