在 Lisp 中应用 reader macro 反对 JSON 语法
什么是 reader macro?
Reader macro 是 Common Lisp 提供的泛滥乏味个性之一,它让语言的使用者可能自定义词法分析的逻辑,使其在读取源代码时,如果遇到了特定的一两个字符,能够调用相应的函数来个性化解决。此处所说的“特定的一两个字符”,被称为 macro character,而“相应的函数”则被称为 reader macro function。举个例子,单引号 '
就是一个 macro character,能够用函数 get-macro-character
来获取它对应的 reader macro function。
CL-USER> (get-macro-character #\')
#<FUNCTION SB-IMPL::READ-QUOTE>
NIL
借助单引号,能够简化一些代码的写法,例如表白一个符号 HELLO
自身能够写成这样。
CL-USER> 'hello
HELLO
而不是上面这种等价但更繁琐的模式。
CL-USER> (quote hello)
HELLO
Common Lisp 中还定义了由两个字符形成的 reader macro,例如用于书写 simple-vector
字面量的 #(
。借助它,如果想要表白一个顺次由数字 1、2、3 形成的simple-vector
类型的对象,不须要显式地调用函数 vector
并传给它 1、2、3,而是能够写成#(1 2 3)
。
反对 JSON 语法后有什么成果?
非法的 JSON 文本不肯定是非法的 Common Lisp 源代码。例如,[1, 2, 3]
在 JSON 规范看来是一个由数字 1、2、3 组成的数组,但在 Common Lisp 中,这段代码会触发 condition。(condition 就是 Common Lisp 中的“异样”、“出情况”了)
CL-USER> (let ((eof-value (gensym)))
(with-input-from-string (stream "[1, 2, 3]")
(block nil
(loop
(let ((expr (read stream nil eof-value)))
(when (eq expr eof-value)
(return))
(print expr))))))
[1 ; Evaluation aborted on #<SB-INT:SIMPLE-READER-ERROR "Comma not inside a backquote." {1003AAD863}>.
这是因为依照 Common Lisp 的读取算法,左方括号 [
和数字 1 都是规范中所指的 constituent character,它们能够组成一个 token,并且最终被解析为一个符号类型的对象。而紧接着的字符是逗号,
,它是一个 terminating macro char,依照规范,如果不是在一个反引号表达式中应用它将会是有效的,因而触发了 condition。
如果存在一个由两个字符 #J
定义的 reader macro、容许开发者应用 JSON 语法来形容紧接着的对象的话,那么就能够写出上面这样的代码。
CL-USER> (progn
(print #jfalse)
(print #jtrue)
(print #j233.666)
(print #jnull)
(print #j[1, 2, [3], [4, 5]])
(print #j{"a": [1, 2, 3]})
(print (gethash "a" #j{"a": [1, 2, 3]})))
YASON:FALSE
YASON:TRUE
233.666d0
:NULL
#(1 2 #(3) #(4 5))
#<HASH-TABLE :TEST EQUAL :COUNT 1 {1003889963}>
#(1 2 3)
#(1 2 3)
显然,用上述语法示意一个哈希表,要比上面这样的代码简略得多
CL-USER> (let ((obj (make-hash-table :test #'equal)))
(setf (gethash "a" obj) #(1 2 3))
obj)
#<HASH-TABLE :TEST EQUAL :COUNT 1 {1003CB7643}>
如何用 reader macro 解析 JSON?
Common Lisp 并没有预置 #J
这个 reader macro,但这门语言容许使用者定义本人的 macro character,因而后面的示例代码是能够实现的。要自定义出 #J
这个读取器宏,须要应用函数 set-dispatch-macro-character
。它的前两个参数别离为形成 macro character 的前两个字符,即#
和J
——其中 J
即使是写成了小写,也会被转换为大写后再应用。第三个参数则是 Lisp 的词法解析器在遇到了 #J
时将会调用的参数。set-dispatch-macro-character
会传给这个函数三个参数:
- 用于读取源代码的字符输出流;
- 形成 macro character 的第二个字符(即
J
); - 非必传的、夹在
#
和J
之间的数字。
百闻不如一见,一段可能实现上一个章节中的示例代码的 set-dispatch-macro-character
用法如下
(set-dispatch-macro-character
#\#
#\j
(lambda (stream char p)
(declare (ignorable char p))
(let ((parsed (yason:parse stream
:json-arrays-as-vectors t
:json-booleans-as-symbols t
:json-nulls-as-keyword t)))
(if (or (symbolp parsed)
(consp parsed))
(list 'quote parsed)
parsed))))
在 set-dispatch-macro-character
的回调函数中,我是用了开源的第三方库 yason
提供的函数 parse
,从输出流stream
中依照 JSON 语法解析出一个值。函数 parse
的三个关键字参数的含意参见这里,此处不再赘述。因为 reader macro 的后果会被用于结构源代码的表达式,因而如果函数 parse
返回了符号或者 cons
类型,为了防止被编译器求值,须要将它们“援用”起来,因而将它们放到第一元素为 quote
的列表中。其它状况下,间接返回 parse
的返回值即可,因而它们是“自求值”的,求值后果是它们本身。
序幕
本文我借助了现成的库 yason
来解析 JSON 格局的字符串,如果你对如何从零开始实现这样的 reader macro 感兴趣的话,能够参考这篇文章。
全文完。
浏览原文