根据 JSON 网站,“JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。它易于人类阅读和编写。它易于机器解析和生成。”
JSON 由 RFC 4627 规定,该 RFC 定义了媒体类型 application/json。
有针对各种语言的 JSON 库,因此它是一种有用的格式。CouchDB 使用 JSON 作为其存储格式和 RESTful 接口;它为某些项目提供了 Mnesia 的替代方案,并且可以从更多语言访问。已经有 Erlang 的 JSON 绑定,例如 LShift 的 rfc4627 模块,但在 2008 年 7 月 24 日,Joe Armstrong 建议值得内置一些函数来将 Erlang 项转换为 JSON 格式以及从 JSON 格式转换。
term_to_json -- convert a term to JSON form
json_to_term -- convert a JSON form to Erlang
在 edoc 中使用的众所周知的类型词汇中添加了三种新类型。
@type json_label() = atom() + binary().
@type json(L, N) = null + false + true
+ N % some kind of number
+ [{}] % empty "object"
+ [{L, json(L,N)}] % non-empty "object"
+ [json(L, N)]. % "array"
| [json(L, N)] | tuple({L, json(L, N)}).
@type json() = json(json_label(), number()).
在 erlang: 模块中添加了四个新函数。
erlang:json_to_term(IO_Data) -> json()
erlang:json_to_term(IO_Data, Option_List) -> json()
类型
IO_Data = iodata()
Option_List = [Option]
Option = {encoding,atom()}
| {float,bool()}
| {label,binary|existing_atom|atom}
json_to_term(X)
等同于 json_to_term(X, [])
。
IO_Data
表示一个字节序列。
encoding 选项说明使用哪种字符编码将这些字节转换为字符。默认编码是 UTF-8。Erlang 中其他地方支持的所有编码都应该在这里支持。JSON 规范提到自动检测编码的可能性;可以检测到的编码包括 UTF-32-BE、UTF-32-LE、UTF-16-BE、UTF-16-LE、UTF-8 和 UTF-EBDIC。编码 “auto” 请求自动检测。
{float,true}
选项表示将所有 JSON 数字转换为 Erlang 浮点数,即使它们看起来像整数。使用此选项,结果的类型为 json(L, float())
。
{float,false}
选项表示将整数转换为整数;这是默认设置。使用此选项,结果的类型为 json(L, number())
。
{label,binary}
选项表示将所有 JSON 字符串转换为 Erlang 二进制文件,即使它们是键:值对中的键。使用此选项,结果的类型为 json(binary(), N)
。这是默认设置。
{label,atom}
选项表示如果可能,将键转换为原子,并将其他字符串保留为二进制文件。使用此选项,结果的类型为 json(json_label(), N)
。
{label,existing_atom}
选项表示如果原子已存在,则将键转换为原子,并将其他键保留为二进制文件。所有其他字符串也保留为二进制文件。使用此选项,结果的类型为 json(json_label(), N)
。
将来可能会添加其他选项。
JSON 到 Erlang 的映射在本节中进行了描述。如果参数不是格式正确的 IO_Data,或者无法解码,或者解码后不遵循 JSON 语法规则,则会产生 badarg 异常。[如果 Erlang 中有区分这些情况的通用约定就好了。]
erlang:term_to_json(JSON) -> binary()
erlang:term_to_json(JSON, Option_List) -> Binary()
类型
JSON = json()
Option_List = [Option]
Option = {encoding,atom()}
| {space,int()}
| space
| {indent,int()}
| indent
这是一个用于生成可移植 JSON 的函数。它不是用于编码任意 Erlang 项的方法。不适合本节中描述的映射方案的项会导致 badarg 异常。JSON RFC 说“对象中的名称应该是唯一的。”违反此规则的 JSON 项也应导致 badarg 异常。
term_to_json(X)
等同于 term_to_json(X, [])
。
将 Erlang 项转换为 JSON 会产生一个(逻辑)字符序列,该序列被编码为一个字节序列,然后作为二进制文件返回。默认编码是 UTF-8;可以使用 encoding 选项覆盖此设置。Erlang 中其他地方支持的所有编码都应该在这里支持。
有两个选项用于控制空格。默认情况下,不生成任何空格。
{space,N}
,其中 N 是一个非负整数,表示在每个冒号和逗号后添加 N 个空格。“space” 等同于 {space,1}
。永远不会插入其他空格。
{indent,N}
,其中 N 是一个非负整数,表示在每个逗号后添加换行符和一些缩进。缩进是每个封闭的 [] 或 {} 的 N 个空格。请注意,这仍然不会导致添加任何其他空格;特别是 ] 和 } 不会出现在行的开头。“indent” 等同于 {indent,1}
。
将来可能会添加其他选项。
关键字 ‘null’、‘false’ 和 ‘true’ 被转换为相应的 Erlang 原子。没有其他完整的 JSON 形式被转换为原子。
如果满足以下条件,则数字将转换为 Erlang 浮点数
看起来像整数但不是 -0 的 JSON 数字将被转换为 Erlang 整数,除非提供了 {float,true}
。
当在 “object” 中作为标签出现时,如果可能,可以根据显式请求将字符串转换为 Erlang 原子。否则,字符串将转换为 UTF-8 编码的二进制文件,无论数据源使用什么编码。空字符串将转换为空二进制文件。
序列将转换为 Erlang 列表。列表中的元素的顺序与原始序列中的顺序相同。
非空的“object”被转换为适合使用 ‘proplists’ 模块处理的 {Key,Value} 对的列表。请注意,proplists: 不要求键必须是原子。没有键:值对的“object”被转换为列表 [{}]
,从而保留了对象始终由非空元组列表表示的不变量。proplists: 模块会将 [{}]
正确地视为不包含任何键。
JSON 形式中的键始终是字符串。仅当满足以下条件时,键才会转换为 Erlang 原子
{label,atom}
或指定了 {label,existing_atom}
且已存在合适的原子;并且目前,只有由 Latin-1 字符组成的名称才能转换为原子。空键 “” 将转换为空原子 ‘’。否则,键将使用 UTF-8 编码转换为二进制文件,无论原始编码是什么。
这意味着如果您现在读取并转换 JSON 项,并将二进制文件保存到某个地方,然后在以后完全 Unicode 的 Erlang 中读取并转换它,您会发现表示形式不同。但是,JSON “object” 中对的顺序没有意义,并且此规范的实现可以自由地以其喜欢的任何顺序报告它们(按给定顺序、反向顺序、排序顺序、按某些哈希值排序等)。在任何特定的 Erlang 版本中,这种转换都是纯函数,但是不同的 Erlang 版本可能会更改对的顺序,因此无论如何您都不能期望从一个版本到另一个版本获得完全相同的项。
请参阅我们不转换为规范形式(例如,通过排序)的原因的理由。
本着“对您接受的内容慷慨,对您生成的内容严格”的精神,接受输入中不带引号的标签可能是一个好主意。您不能只接受任何旧的垃圾,但是允许 Javascript IdentifierNames 将是有意义的。
IdentifierName = IdentifierStart IdentifierPart*.
IdentifierStart = UnicodeLetter | '$' | '_' |
'\u' HexDigit*4
IdentifierPart = IdentifierStart | UnicodeCombiningMark |
UnicodeDigit | UnicodeConnectorPunctuation
显然有一些 JSON 生成器可以做到这一点,因此它会增加价值,但这不是必需的。
原子 ‘null’、‘false’ 和 ‘true’ 将转换为相应的 JSON 关键字。不允许使用其他 Erlang 原子。
Erlang 整数将转换为 JSON 整数。Erlang 浮点数将转换为 JSON 浮点数,尽可能精确。具有整数值的 Erlang 浮点数将以可读回浮点数的方式写入;合适的方法包括附加 “.0” 或 “e0”。
作为某个 Unicode 字符串的 UTF-8 表示形式的 Erlang 二进制文件将转换为字符串。不允许使用其他二进制文件。
所有元素都是元组的 Erlang 列表将转换为 JSON “object”。如果列表是 [{}]
,则将其转换为 “{}”,否则所有元组必须有两个元素,并且第一个元素必须是原子或二进制文件;不允许使用其他元组。对于每个 {Key,Value}
对,键必须是原子或作为某个 Unicode 字符串的 UTF-8 表示形式的二进制文件;该键将转换为 JSON 字符串。输出中键:值对的顺序与列表中 {Key,Value}
对的顺序相同。不允许使用两个等效键的列表。两个二进制文件或两个原子是等效的,当且仅当它们相等时。如果原子和二进制文件将转换为相同的 JSON 字符串,则它们是等效的。
不允许使用 Erlang 元组,除非作为将转换为 JSON “object” 的列表的元素。不允许使用其他元组。
元素不是元组的 Erlang 正确列表将通过按自然顺序转换其元素来转换为 JSON 序列。
不允许使用不正确的列表。
不允许使用其他 Erlang 项。如果您想通过 JSON “隧道”其他 Erlang 项,这很好,但是完全由您来执行您想要的任何转换。
正如 Joe Armstrong 在他的消息中所说,“JSON 似乎无处不在”。它不仅应该得到支持,而且应该简单、高效且可靠地支持。
如上所述,http://www.ietf.org/rfc/rfc4627.txt 定义了 Erlang 应该能够“开箱即用”处理的 application/json 媒体类型。
首先要问的问题是,该接口应该是“值”接口(其中一块数据一次性转换为 Erlang 项),还是像 SGML 解析器提供的经典 ESIS 接口这样的“事件流”接口,由于一些被称为 SAX 的神秘原因。
世界上有这两种接口的空间。这是一个“值”接口,它最适合少量 JSON 数据(例如,小于几兆字节),在这种情况下,等待整个表单再处理任何数据并不成问题。其他人可能想编写一个 “事件流” EEP。
与此问题相关,JSON 文本必须是数组或对象,而不是例如裸数字。或者 JSON RFC 是这么说的。我不知道是否所有 JSON 库都强制执行此操作。由于 JSON 文本必须是 [某些内容] 或 {某些内容},因此 JSON 文本是自我定界的,并且从流中一次消费它们是有意义的。这应该成为此接口的一部分吗?也许是,也许不是。我注意到您可以分离解析
从转换中。所以我将它们分开了。此提案仅解决转换。扩展应该解决解析问题。将其作为事件流 EEP 的一部分可能会更好。
接下来我们考虑转换。往返转换的保真度(X -> Y -> X 应该是一个恒等函数)总是好的。我们能实现吗?
JSON 有
Erlang 有
更准确地说,JSON 语法确实区分整数和浮点数;是 JavaScript(当 JSON 与 JavaScript 一起使用时)未能区分它们。因为我们希望使用 JSON 在 Erlang、Common Lisp、Scheme、Smalltalk 以及最重要的 Python 之间交换数据,所有这些语言都具有这种区分,幸运的是,JSON 语法和 RFC 允许这种区分。
显然,Erlang->JSON->Erlang 会很棘手。仅举一个小的例子,
可以使用 pid_to_list/1
、erlang:port_to_list/1
和 erlang:ref_to_list/1
将 pid、端口和引用转换为文本形式。如果我们想要,一个内置函数当然可以从文本形式转换回来。问题是如何区分这些字符串与其他字符串:什么时候 “<0.43.0>” 是一个 pid,什么时候它是一个字符串?至于 fun,我们就不讨论了。
基本上,将 Erlang 术语转换为 JSON,以便可以将它们重构为相同(或非常相似)的 Erlang 术语,将涉及如下操作
atom -> string
number -> number
binary -> {"type":"binary", "data":[<bytes>]}
list -> <list>, if it's a proper list
list -> {"type":"dotted", "data":<list>, "end":<last cdr>}
tuple -> {"type":"tuple", "data":<tuple as list>}
pid -> {"type":"pid", "data":<pid as string>}
port -> {"type":"port", "data":<port as string>}
ref -> {"type":"ref", "data":<ref as string>}
fun -> {"module":<m>, "name":<n>, "arity":<a>}
fun -> we're pushing things a bit for anything else.
这不是规范的一部分,因为我并不是将 JSON 作为任意 Erlang 数据的表示形式。我的观点是,如果我们真的想,我们*可以*用 JSON 表示(大部分)Erlang 数据,但这并不是一个容易或自然的选择。对于这一点,我们有 Erlang 二进制格式和 UBF。重申一下,我们没有理由相信,一个通过将 JSON 解码为内部形式并重新编码以输出的 JSON->JSON 复制器会保留 Erlang 术语,即使像这样编码。
不,在 Erlang 中支持 JSON 的目的是让 Erlang 程序处理其他人在网络上发送的 JSON 数据,并将 JSON 数据发送到其他程序(如 Web 浏览器中的脚本),这些程序期望使用普通的 JSON。我们需要关心的往返转换是 JSON -> Erlang -> JSON。
在这里我们也遇到了问题。在 Erlang 中表示 {“a”:A, “b”:B} 的明显方法是 [{'a',A},{'b',B}]
,而表示字符串的明显方法是字符列表。但是在 JSON 中,空列表、空“对象”和空字符串都明显不同,因此必须转换为不同的 Erlang 术语。考虑到这一点,这里是 JSON 到 Erlang 映射的第一个版本
- null => the atom 'null'
- false => the atom 'false'
- true => the atom 'true'
- number => a float if there is a decimal point or exponent,
=> the float -0.0 if it is a minus sign followed by
one or more zeros, with or without a decimal point
or exponent
=> an integer otherwise
- string => a UTF-8-encoded binary
- sequence => a list
- object => a list of {Key,Value} pairs
=> the empty tuple {} for an empty {} object
由于 Erlang 目前不允许原子中使用完整的 Unicode 字符范围,如果标签的每个字符都适合 Latin 1,则 Key 应该是一个原子,否则应该是一个二进制。
让我们更仔细地研究一下“对象”。Erlang 程序员习惯于使用 {Key,Value} 对的列表。标准库甚至包含 orddict,它处理的就是这种列表(尽管它们必须排序)。然而,将空对象转换为空元组,但将非空对象转换为空列表,这令人不快,并且根据列表中的内容将列表转换为序列或对象也令人不快。这里令人不快的原因与 TYPES 有关。Erlang 没有静态类型,但这并不意味着类型作为设计工具没有用处,或者某种类似于类型一致性的东西对人们没有用处。Erlang 元组恰好使用大括号只是锦上添花。这个 EEP 的第一稿使用了列表;那完全是 R.A.O’K 自己的工作。后来他注意到乔·阿姆斯特朗认为将“对象”转换为元组是正确的做法。所以下一稿就这样做了。然后又提出了其他替代方案。我目前知道的有
对象是元组
A. {{K1,V1}, ..., {Kn,Vn}}
。
这是将 list_to_tuple/1
应用于 proplist 的结果。没有处理此类事情的库函数,但它们是明确的且相对节省空间的。
B. {object,[{K1,V1}, ..., {Kn,Vn}]}
这是一个用元组包装的 proplist,纯粹是为了将其与其他列表区分开来。这提供了简单的类型测试(对象是元组)和简单的字段处理(它们包含 proplist)。对于标签应该是什么,似乎没有共识,'obj'(无意义的缩写),'json'(但即使是数字、二进制和列表也是 JSON),'object' 似乎是最不令人反感的。
C. {[{K1,V1},...,{Kn,Vn}]}
与 B 类似,但不需要任何标签。
A 和 B 是乔·阿姆斯特朗提出的;我不记得是谁想到了 C。它最近得到了支持者的支持。
对象是列表
D. 空对象是 {}
。
这是我最初的提议。简单但不统一且笨拙。
E. 空对象是 [{}]
。
这来自 Erlang 邮件列表;我已经忘记是谁提出的了。这很棒:对象总是元组列表。
F. 空对象是 'empty'。
与 A 类似,但空间效率略高。
我们可以演示以这些形式中的每一种形式处理“对象”
json:is_object(X) -> is_tuple(X). % A
json:is_object({object,X}) -> is_list(X). % B
json:is_object({X}) -> is_list(X). % C
json:is_object({}) -> true; % D
json:is_object([{_,_}|_]) -> true;
json:is_object(_) -> false.
json:is_object([X|_]) -> is_tuple(X). % E
json:is_object(empty) -> true; % F
json:is_object([{_,_}|_]) -> true;
json:is_object(_) -> false.
其中,A、B、C 和 E 都可以轻松地在子句头中使用,而 E 是唯一一个易于与 proplist 一起使用的。经过一番挠头和摸索,E 成功了。
我们可以考虑添加一个“object”选项
{object,tuple} representation A
{object,pair} representation B.
{object,wrap} representation C.
{object,list} representation E.
对于从 Erlang 到 JSON 的转换,
{T1,...,Tn} 0 or more tuples
{object,L} size 2, 1st element atom, 2nd list
{L} size 1, only element a list
都是可识别的,因此 term_to_json/[1,2]
可以接受所有这些,而无需选项。
我们想要一些这样的选项有一个长期的原因。列表和元组都是错误的。表示 JSON “对象”的正确数据结构是我称之为 “帧” 的结构,乔·阿姆斯特朗称之为 “proper struct”。在未来的某个时候,我们肯定希望将 {object,frame}
作为一种可能性。
假设您正在从不区分整数和浮点数的源接收 JSON 数据?例如 Perl,或者更明显的是,JavaScript 本身。在这种情况下,某些浮点数可能或多或少是偶然地以整数形式编写的。在这种情况下,您可能希望将 JSON 形式的所有数字转换为 Erlang 浮点数。为此提供了 {float,true}
。
从 Erlang 到 JSON 的相应映射是
- atom => itself if it is null, false, or true
=> error otherwise
- number => itself; use full precision for floats,
and always include a decimal point or exponent
in a float
- binary => if the binary is a well formed UTF-8 encoding
of some string, that string
=> error otherwise
- tuple => if all elements are {Key,Value} pairs with
non-equivalent keys, then a JSON "object",
=> error otherwise
- list => if it is proper, itself as a sequence
=> error otherwise
- otherwise, an error
这里存在一个关于键的问题。RFC 说 “对象中的名称应该是唯一的。” 本着“接受时要慷慨,生成时要严格”的精神,我们真的应该检查一下。term_to_json/[1,2]
成功终止的唯一时间应该是输出绝对完美的 JSON 时。我确实考虑过允许重复标签的选项,但是如果我想发送这种非标准数据,我可以发送给谁呢?另一个 Erlang 程序?那么我最好使用外部二进制格式。因此,现在只允许影响空格的选项。以后可能会添加一个选项来指定键值对的顺序,但是不影响语义的选项是合适的。
再仔细想想,看看JSON-RPC 1.1 草案。它在 6.2.4 节 “成员序列” 中说
客户端实现应该努力对过程调用对象的成员进行排序,以便服务器能够采用流式策略来处理内容。至少,客户端应该确保 version 成员首先出现,params 成员最后出现。
这意味着为了符合 JSON-RPC,
term_to_json([{version,<<"1.1">>},
{method, <<"sum">>},
{params, [17,25]}])
不应重新排序对。因此,当前规范说顺序是保留的,并且不提供任何重新排序的方法。如果你想要一个标准的顺序,请在外部对其进行编程。
应该如何报告“重复标签”错误?在 Erlang 中,有两种报告此类错误的方法:引发 'badarg' 异常,或返回 {ok,Result}
或 {error,Reason}
答案。我真的不知道该怎么做。我最终选择了 “引发 badarg”,因为 binary_to_term/1
等方法就是这样做的。
目前,我指定 Erlang 术语使用 UTF-8 并且只使用 UTF-8。这是迄今为止最简单的可能性。但是,我们当然可以添加
{internal,Encoding}
选项来声明要为二进制文件使用或假设的编码。我认为,添加该选项的时间是在证明有需求的时候。
还剩下五个“往返”问题
所有关于空格的信息都丢失了。这不是问题,因为它没有意义。
除非使用 Scheme 报告中描述的技术以高精度进行这些转换,否则浮点数的十进制->二进制->十进制转换可能会引入误差。这是 Erlang 的一个普遍问题,也是 JSON 的一个普遍问题。
Erlang 还有另一个 JSON 库,它总是将 32 位范围之外的整数转换为浮点数。这似乎是一个糟糕的主意。有些语言(Scheme、Common Lisp、SWI Prolog、Smalltalk)的 JSON 库具有 bignum。为什么对我们与它们通信的能力施加任意限制?任何无法将大整数作为整数处理的 JSON 实现(或应该)完全能够自己将这些数字转换为浮点数。当你考虑到另一端的程序本身可能是在 Erlang 中时,这样做似乎特别愚蠢。因此,我们期望如果 T 的类型为 json(binary(),integer())
,那么
json_to_term(term_to_json(T), [{label,binary}])
应该与 T 相同,直到属性对的重新排序。
将字符串转换为二进制文件,然后再将二进制文件转换为字符串,并不总是产生相同的表示形式,但您获得的内容将表示相同的字符串。例如,“\0041” 将读取为 <<65>>
,这将显示为 “A”。
从技术上讲,Unicode “代理项” 不是字符。RFC 允许将基本多语言平面之外的字符写为 UTF-8 序列,或写为 12 个字符的 \uHIGH\uLOWW 代理项对转义。具有裸 \uHIGH 或 \uLOWW 代理项代码点的字符,从技术上讲,不是合法的 Unicode 字符串,因此不应出现此类代码点的 UTF-8 序列。单独的 \uHIGH 或 \uLOWW 转义序列也不应出现;它就像 UTF-8 序列中值为 255 的字节一样,是一个语法错误。我们实际上有两个问题
(a)某些语言可能比较草率,并可能允许字符串内存在单个代理项。Erlang 应该同样草率吗?应该允许这样做吗?
(b) 有些语言(是的,我指的是 Java)实际上并不真正支持 UTF-8,而是先将 Unicode 字符序列分解成 16 位块(UTF-16),然后再将这些块编码为 UTF-8,从而产生绝对非法的 UTF-8。鉴于世界上存在大量的 Java 代码,我们该如何处理这种情况?
在接受时要慷慨:‘utf8’ 解码器应该静默地接受 “UTF-Java”,将单独编码的代理项转换为单个数字代码,并将单独的代理项如同它们是字符一样进行转换。
在生成时要严格:当请求的编码为 ‘utf8’ 时,绝不生成 UTF-Java;应该有一个单独的 ‘java’ 编码可以代替请求。
Hynek Vychodil 强烈主张处理 JSON 标签的唯一可接受方式是使用二进制数据。他反对 {label,atom}
的理由是合理的:如上所述,该选项仅在信任边界内可用。他反对 {label,existing_atom}
的理由是,如果您在一个节点上转换 JSON 形式,然后将 Erlang 项存储在文件中,或通过网络发送,或以任何其他方式使其在另一个节点或另一个时间可用,那么它将与该节点中当时转换的相同 JSON 形式不匹配。这是事实,但还有许多其他的往返问题。使用 {float,true}
转换的数据将与使用 {float,false}
转换的数据不匹配。重复标签的处理方式可能会有所不同。{key,value} 对的顺序尤其可能发生变化。对于所有编程语言和库,如果您想在时间和空间中移动 JSON 数据,唯一可靠的方法是以(可能经过压缩的)JSON 数据形式移动,而不是以其他形式移动。您可以期望在某个时间/地点读取的 JSON 形式与在另一个时间/地点读取的相同形式等效;您不能期望它们是相同的。任何这样做的代码本质上都是有错误的,无论是否使用 {label,existing_atom}
。以下示例说明了该问题是无法根除的。
假设我们有 JSON 形式 “[0.123456789123456789123456789123456]”。不同机器上的两个 Erlang 节点读取它并将其转换为 Erlang 项。其中一个节点将其项发送给另一个节点,该节点对其进行比较。令它惊讶的是,它们并不相同!为什么?嗯,可能是它们使用不同的浮点精度。在 Erlang 的一个主要平台上,支持 128 位浮点数。(此示例需要 128 位。)在其另一个主要平台上,支持 80 位浮点数。(在这两种情况下,我都没有说 Erlang 支持,只是说硬件支持。)实际上,第二个平台的现代版本通常使用 64 位浮点数。让我们假设它们都坚持使用 64 位浮点数。如果其中一个系统是具有非 IEEE 双精度的 IBM/370 呢?因此,假设它们都使用 IEEE 64 位浮点数。它们将使用不同的 C 库进行初始的十进制到二进制转换,因此数字的舍入方式可能会有所不同。如果一个是 Windows,另一个是 Linux 或 Solaris,它们将肯定使用不同的库。如果 Erlang 使用自己的代码(这可能不是一个坏主意),我们仍然在与使用非 IEEE 双精度的机器通信时遇到麻烦,而这些机器仍然在使用中。甚至最初希望在任何地方都获得按位相同结果的 Java,最终也退缩了。
JSON 生成有一个重要问题,那就是应该生成什么空白。由于 JSON 应该“可读”,因此如果可以缩进并且可以保持在合理的行宽内,那就太好了。但是,与外观相反,必须将 JSON 视为二进制格式。无法在字符串内部插入换行符。Javascript 没有类似于 C 的任何东西
我没有考虑的主要事项是 json_to_term/2
的 {label,_}
选项。对于普通的 Erlang 用途,处理
[{name,<<"fred">>},{female,false},{age,65}]
比处理
[{<<"name">>,<<"fred">>},{<<"female">>,false},{<<"age">>,65}]
要好得多(也更有效率)。如果您正在与处理少量已知标签的可信来源进行通信,那就很好。Erlang 可以处理的原子数量有限制。一个小的测试程序循环创建原子并将它们放入列表中,一直顺利运行,直到它过了第 100 万个原子后不久,然后就停滞在那里,似乎无所作为地燃烧着周期。此外,原子表由 Erlang 节点上的所有进程共享,因此对其进行垃圾回收并不像可能的那样便宜。因此,作为一种系统完整性措施,拥有一种 json_to_term
永远不会创建原子的操作模式很有用。但是 Erlang 提供了第三种可能性:有一个内置的 list_to_existing_atom/1
函数,仅当该原子已存在时才返回该原子。否则,它会引发异常。因此有三种情况
{label,binary}
始终将标签转换为二进制数据。这始终是安全的,但始终是笨拙的。由于 Erlang 中存在 «“xxx”» 语法,因此它不是那么笨拙。它是统一且稳定的,因为它不依赖于 Erlang 原子是否支持 Unicode,或者加载了哪些其他模块。
{label,atom}
如果所有字符都允许在原子中使用,则始终将标签转换为原子,否则将其保留为二进制数据。
这对于 Erlang 编程来说更方便。但是,它实际上只能与您信任的合作伙伴一起使用。由于许多通信发生在信任边界内,因此它肯定占有一席之地。如果不是这样,term_to_binary/1 将毫无用处!
{label,existing_atom}
将与现有原子名称匹配的标签转换为这些原子,将所有其他标签保留为二进制数据。如果一个模块提到一个原子,并寻找作为键的该原子,它将找到它。这既安全又方便。它唯一真正的问题是,在不同时间(在同一个 Erlang 节点中)转换的同一个 JSON 项可能会以不同的方式进行转换。这通常无关紧要。
在之前的草案中,我选择 existing_atom
作为默认值,因为这是我最喜欢的选项。这是最简化我想编写的代码的选项。但是,还必须考虑转换问题。一些经过深思熟虑的现有 Erlang JSON 库总是使用二进制数据。
没有 {string,XXX}
选项。这是因为我将 JSON 中的字符串视为“有效负载”,视为正在传输的不可预测的数据,人们不期望它与任何东西匹配。这与标签形成鲜明对比,标签是“结构”而不是数据,人们期望它与很多东西匹配。我确实简要地考虑了 {string,list|binary}
选项,但是现在 Erlang 在匹配二进制数据方面做得如此出色,以至于似乎没有什么意义。
这提出了一个关于二进制数据的普遍问题。喜欢将原子作为标签的原因之一是,原子是唯一存储的,而二进制数据则不是。这延伸到 term_to_binary()
,它会压缩对相同原子的重复引用,而不是对相等二进制数据的重复引用。json_to_term/[1,2]
的 C 实现完全可以跟踪已看到的标签,并共享对重复标签的引用。例如,
[{"name":"root","command":"java","cpu":75.7},
{"name":"ok","command":"iropt","cpu":1.5}
]
– 从 ‘top’ 命令的运行中提取,表明我的 C 编译只占用了机器的一小部分,而 root 运行的某个 Java 程序却占用了大部分 – 将转换为 Erlang,相当于
N = <<"name">>,
M = <<"command">>,
P = <<"cpu">>,
[[{N,<<"root">>},{M,<<"java">>}, {P,75.7}],
[{N,<<"ok">>}, {M,<<"iropt">>},{P, 1.5}]
]
获得原子将使用的许多空间节省。当然,纯 Erlang 程序无法检测到是否正在发生这种共享。如果
term_to_binary(json_to_term(JSON))
保留这种共享,那就太好了。
提出的另一个问题是关于编码的。有些人说他们希望 (a) 允许 UTF-8 以外的输入编码,(b) 以其原始编码而不是 UTF-8 报告字符串,以便 (c) 字符串可以是原始二进制数据的切片。JSON 规范实际上说了什么?第 3 节,编码
JSON 文本应以 Unicode 编码。默认编码为 UTF-8。
这并不像它可能的那样明确。明确提到了 UTF-32 和 UTF-16(两者都有大端和小端形式)。但是 SCSU 是“Unicode”吗?BOCU 呢?UTF-EBCDIC 怎么样?没错,有一种合法的 “Unicode” 编码方式,其中 JSON 特殊字符 []{},:" 不具有其 ASCII 值。似乎没有任何理由认为这是被禁止的,并且在 IBM 大型机上,我希望它会很有用。在有人将 Erlang 移植到 z/Series 机器之前,这主要是学术上的兴趣,但我们不想把自己逼入任何死角。
假设我们确实以其本机编码表示字符串。那又怎样呢?首先,包含任何类型转义序列的字符串无论如何都不能作为源的切片保存。跨越 IO_Data 输入的两个或多个块的字符串也不能。真正的大问题是,没有指示实际的编码是什么,因此我们最终会把来自不同来源的逻辑上相等的字符串视为不相等,把逻辑上不相等的字符串视为相等。
我不希望禁止结果中的字符串成为原始二进制数据的切片。在输入为 UTF-8 并且字符串不包含任何转义符的常见情况下,因此可以这样做,实现应该绝对可以自由地利用这一点。正如本 EEP 当前所规定的那样,它是可以的。我们不能做的是要求这种共享,因为它通常行不通。
有人建议我,term_to_json/[1,2]
的结果最好是 iodata()
而不是 binary()
。任何接受 iodata()
的东西都会对 binary()
感到满意,因此问题在于它对实现更好,是否有可能使用 binary()
必须复制的东西块,而可以使用 iodata()
共享。由于编码问题,我真的不这么认为。现在可能是指出为什么编码在此处完成而不是在其他地方完成的好时机。如果您知道您正在生成将编码为字符集 X 的内容,那么您可以避免生成不在该字符集中的字符。您可以生成 \u 序列来代替。当然,JSON 本身需要 UTF-8,但如果您要通过其他传输方式发送它怎么办?使用 {encoding,ascii}
,您一直都不会遇到麻烦。因此,目前我坚持使用 binary()
。
最后一个问题是这些函数应该进入 erlang: 模块还是其他模块(可能称为 json:)。
如果是另一个模块,则添加其他函数没有任何障碍。例如,我们可以提供一些函数来测试一个项是否是 JSON 项,或者一个 IO_Data 是否表示 JSON 项,或者以某种规范形式呈现结果的替代函数。
如果是另一个模块,那么寻找 JSON 模块的人可能会找到一个。
如果是另一个模块,则无需对核心 Erlang 系统进行任何修改即可轻松地对该接口进行原型设计。
如果是另一个模块,那么不需要此功能的人就不需要加载它。
相反地,
如果是另一个模块,则接口太容易膨胀。我们不需要这样的测试函数,因为我们总是可以捕获现有函数中的 badarg 异常。我们不需要额外的规范化函数,因为我们可以向现有函数添加选项。一些巧妙地鼓励我们减少函数数量的东西是一件好事。
每个 Erlang 程序员都应该熟悉 erlang: 模块,并且在查找任何功能时,都应该从那里开始查找。
Erlang 中已经有 JSON 实现;我们知道使用这样的东西是什么感觉,我们只需要解决实现的细节即可。我们知道它可以实现。现在我们需要一个始终存在、始终相同且尽可能高效的东西。
特别地,我们知道这个特性很有用,并且我们知道在用到它的应用中,它会被频繁使用,所以我们希望它的速度能和 term_to_binary/1 以及 binary_to_term/1
一样快。因此,我们非常希望它能用 C 实现,理想情况下是在虚拟机内部实现。Erlang 动态加载外部代码模块并不容易。
这是一个微妙的平衡。总的来说,我仍然认为将这些函数放在 erlang: 模块中是个好主意,但更多来自双方的理由会更有帮助。
现在 erlang: 模块中没有 term_to_json/N
或 json_to_term/N
函数,所以添加它们应该不会破坏任何东西。这些函数不会被自动导入;必须使用显式的 erlang: 前缀。因此,任何使用这些函数名称的现有代码都不会注意到任何变化。
无。
本文档已置于公共领域。