在 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 #')
#
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 #.
这是因为按照 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))
#
#(1 2 3)
#(1 2 3)
显然,用上述语法表示一个哈希表,要比下面这样的代码简单得多
CL-USER> (let ((obj (make-hash-table :test #'equal)))
(setf (gethash "a" obj) #(1 2 3))
obj)
#
如何用 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 感兴趣的话,可以参考这篇文章。
全文完。
阅读原文
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
相关推荐: java202303java学习笔记第三十二天treeset排序方法的详解2
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net 机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net相关推荐: Linux开发shell命令概述 Shell作用:命令解释器 介于操作系…