14.3 读取宏 (Read-Macros)
7.5 节介绍过宏字符 (macro character)的概念,一个对于 read
有特别意义的字符。每一个这样的字符,都有一个相关联的函数,这函数告诉 read
当遇到这个字符时该怎么处理。你可以变更某个已存在宏字符所相关联的函数,或是自己定义新的宏字符。
函数 set-macro-character
提供了一种方式来定义读取宏 (read-macros)。它接受一个字符及一个函数,因此当 read
碰到该字符时,它返回调用传入函数后的结果。
Lisp 中最古老的读取宏之一是 '
,即 quote
。我们可以定义成:
(set-macro-character #\'
#'(lambda (stream char)
(list (quote quote) (read stream t nil t))))
当 read
在一个普通的语境下遇到 '
时,它会返回在当前流和字符上调用这个函数的结果。(这个函数忽略了第二个参数,第二个参数永远是引用字符。)所以当 read
看到 'a
时,会返回 (quote a)
。
译注: read
函数接受的参数 (read &optional stream eof-error eof-value recursive)
现在我们明白了 read
最后一个参数的用途。它表示无论 read
调用是否在另一个 read
里。传给 read
的参数在几乎所有的读取宏里皆相同:传入参数有流 (stream);接着是第二个参数, t
,说明了 read
若读入的东西是 end-of-file 时,应不应该报错;第三个参数说明了不报错时要返回什么,因此在这里也就不重要了;而第四个参数 t
说明了这个 read
调用是递归的。
(译注:困惑的话可以看看 read 的定义 )
你可以(通过使用 make-dispatch-macro-character
)来定义你自己的派发宏字符(dispatching macro character),但由于 #
已经是一个宏字符,所以你也可以直接使用。六个 #
打头的组合特别保留给你使用: #!
、 #?
、 ##[
、 ##]
、 #{
、 #}
。
你可以通过调用 set-dispatch-macro-character
定义新的派发宏字符组合,与 set-macro-character
类似,除了它接受两个字符参数外。下面的代码定义了 #?
作为返回一个整数列表的读取宏。
(set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(list 'quote
(let ((lst nil))
(dotimes (i (+ (read stream t nil t) 1))
(push i lst))
(nreverse lst)))))
现在 #?n
会被读取成一个含有整数 0
至 n
的列表。举例来说:
> #?7
(1 2 3 4 5 6 7)
除了简单的宏字符,最常定义的宏字符是列表分隔符 (list delimiters)。另一个保留给用户的字符组是 #{
。以下我们定义了一种更复杂的左括号:
(set-macro-character #\} (get-macro-character #\)))
(set-dispatch-macro-character #\# #\{
#'(lambda (stream char1 char2)
(let ((accum nil)
(pair (read-delimited-list #\} stream t)))
(do ((i (car pair) (+ i 1)))
((> i (cadr pair))
(list 'quote (nreverse accum)))
(push i accum)))))
这定义了一个这样形式 #{x y}
的表达式,使得这样的表达式被读取为所有介于 x
与 y
之间的整数列表,包含 x
与 y
:
> #{2 7}
(2 3 4 4 5 6 7)
函数 read-delimited-list
正是为了这样的读取宏而生的。它的第一个参数是被视为列表结束的字符。为了使 }
被识别为分隔符,必须先给它这个角色,所以程序在开始的地方调用了 set-macro-character
。
如果你想要在定义一个读取宏的文件里使用该读取宏,则读取宏的定义应要包在一个 eval-when
表达式里,来确保它在编译期会被求值。不然它的定义会被编译,但不会被求值,直到编译文件被载入时才会被求值。