在 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会传给这个函数三个参数:

  1. 用于读取源代码的字符输出流;
  2. 形成 macro character 的第二个字符(即J);
  3. 非必传的、夹在#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 感兴趣的话,能够参考这篇文章。

全文完。

浏览原文