Skip to content

LISP - 读取时宏

读取宏(Reader macro,或称“读宏”)是一项强大功能,允许程序员扩展 Lisp 自身的语法。它修改了 Lisp 读取器(Lisp reader)的行为——读取器是将文本源代码解析为 Lisp 对象的组件。通过定义自定义读取宏,您可以为数据结构创建新的、简洁的表示法,甚至可以将领域特定语言(DSLs)直接嵌入到您的 Lisp 代码中。

要理解读取宏,了解一段 Lisp 代码所经历的阶段至关重要:

  • 读取时(Read-Time):Lisp 读取器消耗字符流(来自文件或 REPL),并生成 Lisp 对象(S-表达式)。读取宏在此阶段操作。
  • 编译时(Compile-Time):Lisp 编译器接收由读取器生成的 S-表达式。常规宏(使用 defmacro 定义的宏)在此阶段展开代码。
  • 运行时(Run-Time):最终编译好的代码被执行。
  • 语法扩展:创建自定义的语法糖。单引号(')是最著名的例子:'foo 是 (quote foo) 的读取时语法糖。
  • 自定义字面量:为自定义数据类型定义字面量。例如,您可以为正则表达式定义一个读取器,如 #/\d+/。
  • 读取时求值:在读取过程中执行代码。#.(井号点)宏就是这样做的,它会求值一个 Lisp 形式,并将其结果插入到正在读取的代码中。

读取宏是一个函数,您将其与读取表(readtable)中的一个或多个字符关联起来。当 Lisp 读取器遇到一个附加了宏函数的字符时,它会调用该函数。该函数接收输入流和触发字符作为参数。然后,它负责解析后续输入并返回其所代表的 Lisp 对象。

单引号(')是一个内置的读取宏。让我们研究一下您如何自己实现它以理解其机制。这纯粹是学术练习,因为在实践中您永远不需要这样做。

;; 1. 定义将处理宏字符的函数。
;; 它接受流和触发它的字符作为参数。
(defun my-quote-reader (stream char)
"读取下一个 Lisp 对象,并将其封装在一个 (quote ...) 形式中。"
;; 我们不需要 'char' 参数,因此忽略它。
(declare (ignore char))
;; 构造列表:(quote <对象>)
(list 'quote (read stream t nil t)))
;; 2. 将函数与读取表中的一个字符关联起来。
;; 这里,我们(重新)定义了用于 '#' 字符后跟撇号的宏。
(set-macro-character #\' #'my-quote-reader)
;; 3. 现在,我们可以测试它。
;; 在 REPL(或文件中)输入 \'foo 现在将被读取为 (quote foo)。
;; 让我们验证读取器产生的结果:
(read-from-string "\'my-symbol")

执行 (read-from-string "\'my-symbol") 返回两个值:读取到的对象和字符串中下一个字符的位置。

(QUOTE MY-SYMBOL) ; 读取到的 Lisp 对象
10 ; 读取后在字符串中的位置
  • 自定义语法:为常见模式创建简写。一个项目可能会定义 #{ ... } 来读取为哈希集合字面量。
  • 嵌入数据格式:在读取时直接将 JSON 或简化的 key: value 对表示法等数据格式解析为 Lisp 数据结构。
  • 编译时常量:#. 读取宏对于将计算结果嵌入到编译代码中很有用,例如 (defconstant +pi+ #.(- (acos 0) (acos 0)))。

陷阱:覆盖标准字符,例如 ( 或 ",可能会完全破坏 Lisp 读取器。最佳实践是使用分发宏字符(dispatching macro characters)(如 #)来创建您自己独特的语法(例如 #[、#< 等),以避免冲突。