在 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> 'helloHELLO
而不是上面这种等价但更繁琐的模式。
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 感兴趣的话,能够参考这篇文章。
全文完。
浏览原文