根據 JSON 網站,「JSON(JavaScript 物件表示法)是一種輕量級的資料交換格式。它易於人類讀寫,也易於機器解析和產生。」
JSON 由 RFC 4627 規範,其中定義了媒體類型 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 浮點數:
看起來像是整數的 JSON 數字(-0 除外)會轉換為 Erlang 整數,除非提供了 {float,true}
。
當作為「物件」中的標籤出現時,如果可以,可以根據明確的要求將字串轉換為 Erlang 原子。否則,無論資料來源使用何種編碼,字串都會轉換為 UTF-8 編碼的二進位制。空字串會轉換為空二進位制。
序列會轉換為 Erlang 清單。清單中的元素順序與原始序列中的元素順序相同。
非空的「物件」會轉換為適用於使用 ‘proplists’ 模組處理的 {Key,Value} 對清單。請注意,proplists: 並不要求鍵必須是原子。沒有鍵值對的「物件」會轉換為清單 [{}]
,保留物件始終由非空元組清單表示的不變性。proplists: 模組會將 [{}]
正確視為沒有保留任何鍵。
JSON 形式中的鍵始終是字串。鍵只有在符合下列條件時才會轉換為 Erlang 原子:
{label,atom}
或已指定 {label,existing_atom}
,並且已存在適當的原子;以及目前,只有由 Latin-1 字元組成的名稱才能轉換為原子。空鍵 "" 會轉換為空原子 ''。否則,鍵會轉換為二進位制,使用 UTF-8 編碼,無論原始編碼為何。
這表示,如果您現在讀取並轉換 JSON 項,然後將二進位制儲存在某個位置,然後在之後的完全 Unicode Erlang 中讀取並轉換它,您會發現表示方式有所不同。但是,JSON「物件」中成對的順序並不重要,並且此規格的實作可以自由地以任何它喜歡的順序報告它們(按給定順序、反向順序、排序順序、按某些雜湊排序,您自己選擇)。在任何特定的 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”。
Erlang 二進位制是某些 Unicode 字串的 UTF-8 表示法,會轉換為字串。不允許使用其他二進位制。
其所有元素都是元組的 Erlang 清單會轉換為 JSON「物件」。如果清單是 [{}]
,則會轉換為 “{}”,否則,所有元組都必須有兩個元素,第一個元素必須是原子或二進位制;不允許使用其他元組。對於每個 {Key,Value}
對,鍵必須是原子或二進位制,它是某些 Unicode 字串的 UTF-8 表示法;鍵會轉換為 JSON 字串。輸出中鍵值對的順序與清單中 {Key,Value}
對的順序相同。不允許使用兩個相等的鍵的清單。兩個二進位制或兩個原子在相等時才相等。如果原子和二進位制會轉換為相同的 JSON 字串,則它們是相等的。
不允許使用 Erlang 元組,除非它們是將轉換為 JSON「物件」的清單的元素。不允許使用其他元組。
元素不是元組的 Erlang 正確清單會依自然順序轉換其元素,以轉換為 JSON 序列。
不允許使用不正確的清單。
不允許使用其他 Erlang 項。如果您想透過 JSON「通道」傳輸其他 Erlang 項,那很好,但完全由您自行進行任何所需的轉換。
正如 Joe Armstrong 在他的訊息中所說,「JSON 似乎無處不在」。它不僅應該受到支援,而且應該以簡單、高效且可靠的方式受到支援。
如上所述,http://www.ietf.org/rfc/rfc4627.txt 定義了 Erlang 應該能夠「開箱即用」處理的 application/json 媒體類型。
第一個問題是介面應該是「值」介面(其中一塊資料一次轉換為 Erlang 項)還是「事件串流」介面,就像 SGML 剖析器提供的傳統 ESIS 介面一樣,由於某些神秘的原因,現在被稱為 SAX。
世界上兩種介面都有空間。這是一個「值」介面,最適合少量的 JSON 資料(例如少於幾 MB),在這種情況下,等待整個表單然後再處理它的延遲不是問題。其他人可能想寫一個「事件串流」EEP。
與此問題相關的是,JSON 文字必須是陣列或物件,而不是例如純數字。JSON RFC 這麼說。我不知道是否所有 JSON 函式庫都強制執行此規則。由於 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,它僅使用此類列表 (儘管它們必須排序)。但是,將空物件轉換為空元組,但將非空物件轉換為空列表,這有點令人反感,而且將列表轉換為序列或物件,這取決於它們內部是什麼,也有點令人反感。這裡令人反感的事情與類型有關。Erlang 沒有靜態類型,但這並不意味著類型作為一種設計工具沒有用,或者類似於類型一致性的東西對人們沒有用。Erlang 元組恰好使用大括號只是錦上添花。本 EEP 的第一個草稿使用了列表;這完全是 R.A.O’K 自己完成的。然後他注意到 Joe Armstrong 認為將「物件」轉換為元組是正確的做法。所以下一個草稿就這樣做了。然後提出了其他替代方案。我目前知道的有
物件是元組
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 是 Joe Armstrong 提出的;我不記得是誰想到了 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「物件」的正確資料結構是我稱之為「框架」而 Joe Armstrong 稱之為「正確結構」的結構。在未來的某個時候,我們肯定會希望將 `{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 說「物件中的名稱應該是唯一的」。本著「寬容地接受,嚴格地生成」的精神,我們真的應該檢查這一點。只有當輸出是絕對完美的 JSON 時,`term_to_json/[1,2]` 才會成功終止。我曾經考慮過一個允許重複標籤的選項,但是如果我想發送這種非標準的資料,我可以發送給誰?另一個 Erlang 程式?那麼我最好使用外部二進制格式。因此,現在允許的唯一選項是影響空白的選項。稍後可能會添加一個選項來以某種方式指定 key:value 配對的順序,但是不影響語義的選項是適當的。
仔細想想,看看 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}` 回答。我真的不太確定這裡該怎麼做。我最終使用了 'raise 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 可以處理的原子數量有限制。一個小型測試程式循環建立原子並將它們放入清單中,順利地運行,直到第一百萬個原子後不久,然後就掛在那裡,顯然浪費週期而沒有任何進展。此外,原子表由 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:
)中。
如果是其他模組,那麼添加其他函數就沒有障礙了。例如,我們可以提供函數來測試一個 term 是否為 JSON term,或者一個 IO_Data 是否表示一個 JSON term,或者以某種標準形式呈現結果的替代函數。
如果是其他模組,那麼尋找 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:
前綴。因此,任何使用這些函數名稱的現有程式碼都不會注意到任何變化。
無。
本文檔已置於公有領域。