Skip to content

LISP - 宏字符

Common Lisp 最强大和独特的特性之一是其可扩展的解析器,称为读取器(reader)。宏字符(macro characters)是在读取阶段触发自定义解析逻辑的特殊字符,这种逻辑称为读取宏(reader macro)。这允许您扩展 Lisp 自身的语法,创建领域特定语言(DSLs)或便捷的速记符号。

在 Lisp 代码被求值或编译之前,读取器会处理源代码文本并将其转换为 Lisp 对象(S-表达式)。当读取器遇到宏字符时,它不会将其视为普通字符读取。相反,它会调用关联的读取宏函数,该函数随后会接管控制权,从输入流中消耗后续文本,并返回一个 Lisp 对象。

您可能已经熟悉许多标准宏字符:

  • ((左括号):开始读取列表。递归读取对象,直到找到匹配的 )。
  • )(右括号):终止列表读取。
  • '(单引号):quote 特殊形式的简写。'foo 被读取为 (quote foo)。
  • ;(分号):开始单行注释。读取器会忽略该行的其余部分。
  • "(双引号):开始和结束读取字符串字面量。
  • ``` ` “(反引号/准引用号):启动一个准引用(quasiquote)模板,允许进行结构化代码生成并选择性地求值。
  • ,(逗号):在反引号表达式内部使用,用于标记要进行求值的子表达式。
  • #(井号/分发字符):一个分发宏字符(dispatching macro character)。它的作用取决于其后的字符(例如,#' 用于函数对象,#\ 用于字符字面量,#(...) 用于向量)。

您可以使用 set-macro-character 定义自己的宏字符。此函数将一个字符与一个将由读取器执行的处理函数关联起来。

示例:一个类似 JSON 的对象读取器

Section titled “示例:一个类似 JSON 的对象读取器”

让我们创建一个简单的读取宏。我们将使 { 成为一个宏字符,它能读取类似 {"key": "value"} 的 JSON 风格对象,并将其转换为 Lisp 哈希表。

;; 首先,定义读取器函数。它接受流和字符作为参数。
(defun json-object-reader (stream char)
(declare (ignore char)) ; 我们不需要字符 '{' 本身
;; 核心逻辑是读取一个键值对列表...
(let ((pairs (read-delimited-list #\} stream t)))
;; ...并将其转换为创建哈希表的表达式。
`(let ((table (make-hash-table :test #'equal)))
,@(loop for (key value) in pairs
collect `(setf (gethash ,key table) ,value))
table)))
;; 现在,将字符 '{' 与我们的读取器函数关联起来。
(set-macro-character #\{ #'json-object-reader)
;; 我们还需要告诉读取器,字符 '}' 会关闭我们的形式。
(set-macro-character #\} (get-macro-character #\) nil))
;; 让我们使用新语法!
(let ((my-data '{"name" "John Doe" "age" 30}))
(print (gethash "name" my-data)))
  1. 当读取器看到 { 时,它会调用 json-object-reader。
  2. (read-delimited-list #\} ...) 会读取大括号内的所有形式,直到找到一个 }。
  3. 该函数使用反引号构造一段 Lisp 代码。这段代码在求值时将创建并填充一个哈希表。
  4. 读取器将原始的 {...} 文本替换为这段生成的代码。
  5. 最后,Lisp 求值这段生成的代码,创建哈希表。
"John Doe"

尽管读取器功能极其强大,但修改它时应格外小心:

  • 可读性:非标准语法可能使代码难以被他人(和您未来的自己)阅读和理解。
  • 可移植性:过度使用自定义读取宏可能会将您的代码绑定到特定的环境或项目。
  • 用例:它们最适合用于创建嵌入式 DSL 或在明确定义的上下文中,真正提高生产力的重要语法便利。

在现代实践中,通常更倾向于使用常规的 Lisp 宏(defmacro)来创建 DSL,因为它们作用于标准 Lisp 语法,并且更容易理解。读取宏仅在您确实需要改变语言基本语法时才应使用。