LISP - 读取时宏
Lisp - 读取宏
Section titled “Lisp - 读取宏”读取宏(Reader macro,或称“读宏”)是一项强大功能,允许程序员扩展 Lisp 自身的语法。它修改了 Lisp 读取器(Lisp reader)的行为——读取器是将文本源代码解析为 Lisp 对象的组件。通过定义自定义读取宏,您可以为数据结构创建新的、简洁的表示法,甚至可以将领域特定语言(DSLs)直接嵌入到您的 Lisp 代码中。
关键概念:Lisp 求值流程
Section titled “关键概念:Lisp 求值流程”要理解读取宏,了解一段 Lisp 代码所经历的阶段至关重要:
- 读取时(Read-Time):Lisp 读取器消耗字符流(来自文件或 REPL),并生成 Lisp 对象(S-表达式)。读取宏在此阶段操作。
- 编译时(Compile-Time):Lisp 编译器接收由读取器生成的 S-表达式。常规宏(使用
defmacro定义的宏)在此阶段展开代码。 - 运行时(Run-Time):最终编译好的代码被执行。
读取宏的目的
Section titled “读取宏的目的”- 语法扩展:创建自定义的语法糖。单引号(
')是最著名的例子:'foo是(quote foo)的读取时语法糖。 - 自定义字面量:为自定义数据类型定义字面量。例如,您可以为正则表达式定义一个读取器,如
#/\d+/。 - 读取时求值:在读取过程中执行代码。
#.(井号点)宏就是这样做的,它会求值一个 Lisp 形式,并将其结果插入到正在读取的代码中。
读取宏的工作原理
Section titled “读取宏的工作原理”读取宏是一个函数,您将其与读取表(readtable)中的一个或多个字符关联起来。当 Lisp 读取器遇到一个附加了宏函数的字符时,它会调用该函数。该函数接收输入流和触发字符作为参数。然后,它负责解析后续输入并返回其所代表的 Lisp 对象。
示例:重新实现单引号
Section titled “示例:重新实现单引号”单引号(')是一个内置的读取宏。让我们研究一下您如何自己实现它以理解其机制。这纯粹是学术练习,因为在实践中您永远不需要这样做。
;; 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 ; 读取后在字符串中的位置读取宏的实际应用
Section titled “读取宏的实际应用”- 自定义语法:为常见模式创建简写。一个项目可能会定义
#{ ... }来读取为哈希集合字面量。 - 嵌入数据格式:在读取时直接将 JSON 或简化的
key: value对表示法等数据格式解析为 Lisp 数据结构。 - 编译时常量:
#.读取宏对于将计算结果嵌入到编译代码中很有用,例如(defconstant +pi+ #.(- (acos 0) (acos 0)))。
常见错误和调试
Section titled “常见错误和调试”陷阱:覆盖标准字符,例如 ( 或 ",可能会完全破坏 Lisp 读取器。最佳实践是使用分发宏字符(dispatching macro characters)(如 #)来创建您自己独特的语法(例如 #[、#< 等),以避免冲突。