乍听之下,不无道理;仔细揣摩,胡说八道

0%

在CommonLisp中支持JSON语法

在 Lisp 中使用 reader macro 支持 JSON 语法

什么是 reader macro?

Reader macro 是 Common Lisp 提供的众多有趣特性之一,它让语言的使用者能够自定义词法分析的逻辑,使其在读取源代码时,如果遇到了特定的一两个字符,可以调用相应的函数来个性化处理。此处所说的“特定的一两个字符”,被称为 macro character,而“相应的函数”则被称为 reader macro function。举个例子,单引号'就是一个 macro character,可以用函数get-macro-character来获取它对应的 reader macro function。

1
2
3
CL-USER> (get-macro-character #\')
#<FUNCTION SB-IMPL::READ-QUOTE>
NIL

借助单引号,可以简化一些代码的写法,例如表达一个符号HELLO本身可以写成这样。

1
2
CL-USER> 'hello
HELLO

而不是下面这种等价但更繁琐的形式。

1
2
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 中的“异常”、“出状况”了)

1
2
3
4
5
6
7
8
9
10
11
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 语法来描述紧接着的对象的话,那么就可以写出下面这样的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

显然,用上述语法表示一个哈希表,要比下面这样的代码简单得多

1
2
3
4
5
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用法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
(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 感兴趣的话,可以参考这篇文章

全文完。

Liutos wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
你的一点心意,我的十分动力。