LISP - 编译器宏
Lisp - 编译器宏
Section titled “Lisp - 编译器宏”在 Common Lisp 中,宏(macros)是元编程(metaprogramming)的基础特性,允许您在代码编译前对其进行转换和生成。编译器宏(compiler macro)是该系统中的一种专用工具,专门为优化而设计。本章探讨了标准宏(defmacro)和编译器宏(define-compiler-macro)之间的关系和区别。
核心区别:语法与优化
Section titled “核心区别:语法与优化”理解何时以及为何应用每种类型的宏是关键:
- 标准宏(
defmacro):它们的主要目的是语法扩展。它们允许您创建新的控制结构、领域特定语言(DSL)或仅仅减少样板代码(boilerplate code)。标准宏在编译器主工作开始之前,将一个 S-表达式转换为另一个 S-表达式。编译器只看到扩展后的代码。 - 编译器宏(
define-compiler-macro):它们的目的在于优化。它们为函数调用提供了一个替代的、更高效的实现。Lisp 编译器在其优化阶段调用编译器宏,为其提供一个更快代码序列的“建议”。编译器可以选择忽略此建议。
定义和使用宏
Section titled “定义和使用宏”让我们看看它们是如何定义的以及在哪里使用。
标准宏(defmacro)
Section titled “标准宏(defmacro)”标准宏使用 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)
Section titled “编译器宏(define-compiler-macro)”编译器宏使用 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` 是假设的快速函数。;; 原理是用特定的、更快的调用替换泛型调用。主要区别总结
Section titled “主要区别总结”| 特性 | defmacro (标准宏) | define-compiler-macro (编译器宏) |
|---|---|---|
| 目的 | 语法抽象、DSL、代码生成 | 性能优化 |
| 运行时间 | 编译之前(在宏展开阶段) | 编译期间(在优化阶段) |
| 输入 | 参数(未求值的 S-表达式) | 整个函数调用形式(例如,(my-func a b)) |
| 是函数吗? | 否。它定义的是代码转换,而不是可调用的函数。 | 否。它是对现有可调用函数的转换提示。 |
| 编译器能否忽略? | 否。展开是强制性的。 | 是。编译器可以忽略该提示并使用原始函数定义。 |
| 何时使用 | 当您需要 Lisp 中不存在的新语法时。 | 当一个函数对于特定参数类型或值有更高效的实现,并且这可以在编译时确定时。 |
调试和最佳实践
Section titled “调试和最佳实践”- 调试宏:在您的 REPL(Read-Eval-Print Loop,读取-求值-打印循环)中使用
macroexpand-1和macroexpand来查看您的标准宏会展开成什么。这对于查找生成代码中的错误至关重要。 - 谨慎使用:编译器宏是一个高级特性。除非您通过性能分析(profiling)确定了性能瓶颈,否则不要使用它们。过早优化可能导致代码复杂且难以调试。
- 保持语义:编译器宏不得改变原始函数的行为。它应该是一种纯粹的优化。
- 安全回退:始终确保当编译器宏无法应用优化时,返回原始形式。这能确保您的代码保持正确。