第 17 章 读取宏(read-macro)
第 17 章 读取宏(read-macro)
在 Lisp 表达式的一生中,有三个最重要的时刻,分别是读取期(read-time),编译期(compile-time) 和运行期(runtime)。运行期由函数左右。宏给了我们在编译期对程序做转换的机会。本章讨论读取宏(read-macro),它们在读取期发挥作用。
17.1 宏字符
按照 Lisp 的一般哲学,你可以在很大程度上控制 reader
。它的行为是由那些可随时改变的属性和变量控制的。Reader 可以在几个层面上编程。若要改变其行为,最简单的方式就是定义新的宏字符。
宏字符(macro character) 是一种被 Lisp reader
特殊对待的字符。举个例子,小写字母 a
的处理方式和小写字母 b
是一样的,它们都由常规的处理方式处理。但左括号就有些不同:它告诉 Lisp 开始读取一个列表。
每个这样的字符都有一个与之关联的函数,告诉 Lisp reader
当遇到该字符的时候做什么。你可以改变一个已有的宏字符的关联函数,或者定义你自己的新的宏字符。
内置函数 set-macro-character
提供了一种定义读取宏的方式。它接受一个字符和一个函数,以后当 read
遇到这个字符时,它就返回调用该函数的结果。
[示例代码 17.1] '(引号)的可能定义
(set-macro-character #\'
#'(lambda (stream char)
(declare (ignore char))
(list 'quote (read stream t nil t))))
Lisp 中最古老的读取宏之一是单引号 '
,即引用。你也可以不用 '
,而总是将 'a
写成 (quote a)
,但这将会非常烦人, 而且会降低代码的可读性。引用读取宏使 (quote a)
可以简写成 'a
。我们可以用 [示例代码 17.1] 中的方法实现它。当 read
在一个普通的上下文中(例如,不在 "a'b"
或 |a'b|
中) 遇到 '
时,它将返回在当前流和字符上调用这个函数的结果。(该函数忽略了它的第二个形参,因为它总是那个引用字符。) 所以当 read
看到 'a
时,它将返回 (quote a)
。
read
的最后三个参数分别控制:是否在碰到 end-of-file
时报错,如果不报错的话返回什么值,以及这个 read
调用是否是发生在 read
调用中的(译者注:关于 read
的最后一个参数(recursive-p),详见 CLTL 中对 read
的解释。) 。在几乎所有的读取宏里,第二和第四个参数都应该是 t
,所以第三个参数也就无关紧要了。
读取宏和常规宏一样,其实质都是函数。和生成宏展开的函数一样,和宏字符相关的函数,除了作用于它读取的流以外,不应该再有其他副作用。Common Lisp 明确声明:一个与宏字符相关联的函数何时被执行,或者被执行几次 Common Lisp 对其将不给予保证。(见 CLTL2 的 543 页。)
宏和读取宏在不同的阶段分析和观察你的程序。宏在程序中发生作用时,它已经被 reader 解析成了 Lisp 对象,而读取宏在程序还是文本的阶段时,就对它施加影响了。尽管如此,通过在这些文本上调用 read ,一个读取宏,如果它愿意的话,同样可以得到解析后的 Lisp 对象。这样说来,读取宏至少和常规宏一样强有力。
事实上,读取宏至少在两方面比常规宏更为强大。读取宏可以影响 Lisp 读取的每一样东西,而宏只是在代码里被展开。并且,由于读取宏通常递归地调用 read,一个类似:
''a
的表达式将变成:
(quote (quote a))
而如果我们试图用一个普通的宏来为 quote
定义缩略语的话:
(defmacro q (obj)
'(quote ,obj))
它在某些情况下可以正常工作:
> (eq 'a (q a))
T
但在被嵌套使用时就不行了。例如:
(q (q a))
将展开成:
(quote (q a))
译者注:解决这个问题的正确方法是定义一个编译器宏(compiler-macro)。Common Lisp 内置的 define-compiler-macro
用于定义编译器宏,详见 CLTL 中关于此操作符的说明。
17.2 dispatching
宏字符
#'
和其他 #
开头的读取宏一样,是一种称为 dispatching
读取宏的实例。这些读取宏以两个字符出现,其中第一个字符称为 dispatch
字符。这类宏的目的,简单说就是尽可能地充分利用 字符集;如果只有单字符读取宏的话,那么读取宏的数量就会受限于字符集的大小。
你可以(通过使用 make-dispatch-macro-character
) 来定义你自己的 dispatching
宏字符,但由于 #
已经定义了,所以你也可以直接用它。一些 #
打头的组合就是特意为你保留的;其他的那些,如果 Common Lisp 还没有给它们赋予含义的话,也可以拿来用。完整的列表可见 CLTL2 的第 531 页。
[示例代码17.2] 一个用于常数函数的读取宏
(set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(declare (ignore char1 char2))
'#'(lambda (&rest ,(gensym))
,(read stream t nil t))))
新的 dispatching
宏字符组合可以通过调用 set-dispatch-macro-character
函数定义,除了接受两个字符参数以外和 set-macro-character
的用法差不多。一个预留给程序员的组合是 #?
。[示例代码 17.2] 显示了如何将这个组合定义成一个用于常数函数的读取宏。现在 #?2
将被读取为一个函数,其接受任意数量的参数,并且返回 2
。例如:
> (mapcar #?2 '(a b c))
(2 2 2)
这个例子里定义的新操作符看起来相当无聊,但在使用了很多函数型参数的程序里,常常会用到常数函数。
事实上,有些方言提供了一个名叫 always
的内置函数,专门用来定义它们。
注意到在这个宏字符的定义中使用宏字符是完全没有问题的:和任何 Lisp 表达式一样,当这个定义被读取以后这些宏字符就都消失了。在 #?
的后面使用宏字符也是可以的。因为 #?
的定义调用了read
,所以诸如 '
和 #'
此类宏字符也可以正常使用:
> (eq (funcall #?'a) 'a)
T
> (eq (funcall #?#'oddp) (symbol-function 'oddp))
T
17.3 定界符
[示例代码 17.3] 一个定义定界符的读取宏
(set-macro-character #\] (get-macro-character #\)))
(set-dispatch-macro-character #\# #\[
#'(lambda (stream char1 char2)
(declare (ignore char1 char2))
(let ((accum nil)
(pair (read-delimited-list #\] stream t)))
(do ((i (ceiling (car pair)) (1+ i)))
((> i (floor (cadr pair)))
(list 'quote (nreverse accum)))
(push i accum)))))
除了简单的宏字符,定义得最多的宏字符要算列表定界符了。另一个为用户预留的组合字符是 #[
。[示例代码 17.3] 给出的例子,显示了把这个字符定义成一个更复杂的左括号的方法。它定义形如 #[x y]
的表达式,使得这样的表达式被读取为在 x
到 y
的闭区间上所有整数的列表:
> #[2 7]
(2 3 4 5 6 7)
这个读取宏里,唯一的新东西是对 read-delimited-list
的调用,这个函数是一个完全为这种情况度身定制的内置函数。它的第一个参数是那个被当作列表结尾的字符。有其名才能行其实,为了把]
识别成定界符,程序在开始的地方调用了 set-macro-character
。
[示例代码17.4] 一个用于定义定界符读取宏的宏
(defmacro defdelim (left right parms &body body)
'(ddfn ,left ,right #'(lambda ,parms ,@body)))
(let ((rpar (get-macro-character #\))))
(defun ddfn (left right fn)
(set-macro-character right rpar)
(set-dispatch-macro-character #\# left
#'(lambda (stream char1 char2)
(declare (ignore char1 char2))
(apply fn
(read-delimited-list right stream t))))))
多数潜在的定界符读取宏都将在很大程度上重复 [示例代码 17.3] 中的代码。或许可以写个宏,让它从这些机制中提炼出更抽象的接口,以简化代码。[ 示例代码 17.4] 就是一个实现,我们可以像它那样定义一个实用工具,用其定义定界符读取宏。宏 defdelim
接受两个字符,一个参数列表,以及一个代码主体。参数列表和代码主体隐式地定义了一个函数。一个对 defdelim 的调用将首个字符定义为 dispatching
读取宏,它读取到第二个字符为止,然后将这个函数应用到它读到的东西,并返回其结果。
无独有偶,[示例代码 17.3] 中的函数体也迫切需要一个实用工具,事实上,这个实用工具已经定义过了:见 4.5 节的 mapa-b
。使用 defdelim
和 mapa-b
,[示例代码 17.3] 中定义的读取宏现在只需写成:
(defdelim #\[ #\] (x y)
(list 'quote (mapa-b #'identity (ceiling x) (floor y))))
定界符读取宏也可以用来做函数复合。第5.4 节定义了一个用于函数复合的操作符:
> (let ((f1 (compose #'list #'1+))
(f2 #'(lambda (x) (list (1+ x)))))
(equal (funcall f1 7) (funcall f2 7)))
T
当我们复合像 list
和 1+
这样的内置函数时,没有理由等到运行期才去对 compose 的调用求值。第 5.7 节建议一个替代方案;通过给一个 compose
表达式前缀 sharp-dot
读取宏:
#.(compose #'list #'1+)
我们可以令其在读取期就被求值。
[示例代码 17.5]:一个用于函数型复合的读取宏
(defdelim #\{ #\} (&rest args)
'(fn (compose ,@args)))
这里我们给出一个与之类似但更清晰的解决方案。[示例代码 17.5] 中定义的读取宏定义了一个 #{ }
形式的表达式,这个表达式将被读取成 的复合。这样:
> (funcall #{list 1+} 7)
(8)
它生成一个对 fn
(15.1 节) 的调用,该调用在编译期创建函数。
17.4 这些发生于何时
最后,澄清一个可能造成困惑的问题应该会有所帮助。如果读取宏是在常规宏之前作用的话,那么宏是怎样展开成含有读取宏的表达式的呢?例如,这个宏:
(defmacro quotable ()
'(list 'able))
会生成一个带有引用的展开式。还是说它没有生成?事实上,真相是:这个宏定义中的两个引用在这个 defmacro
表达式被读取时,就都被展开了,展开结果如下
(defmacro quotable ()
(quote (list (quote able))))
通常,在宏展开式里包含读取宏是没有什么问题的。因为一个读取宏的定义在读取期和编译期之间将不会(或者说不应该) 发生变化。