映射 (Maps) 和此 EEP 的發展歷程漫長,而且絕非一帆風順。當我們約莫兩三年前開始在 OTP 內部討論時,我對於我希望映射 (Maps) 成為什麼樣子有著非常清晰的藍圖。此 EEP 與那個願景相似,但也吸收了來自 OTP 內部和外部的許多其他想法。
這個想法是一種資料類型,一種語法感知的鍵值關聯映射,並具有模式匹配的功能。其語法類似於記錄 (records),但沒有編譯時依賴性的麻煩,並且可以使用任意的 Term 作為鍵。順序並不重要,並且可以使用雜湊陣列映射樹 (Hash-Array-Mapped-Trie) 來實現,具有良好的效能和記憶體權衡。這是一種與取代記錄 (records) 不同的方法。它旨在取代適用的記錄 (records),在其他方面則不作為取代,而是其獨特的「事物」。
從社群中,有許多關於類似映射 (Map) 資料類型的需求和一些建議。其中一個突出的建議當然是 Richard O’Keefe 的 Frames 提案。這是我見過最完整的提案,而且經過深思熟慮。它的目標是取代記錄 (records),並且該提案很好地實現了這個目標。
取代記錄 (records) 顧名思義就是取代。這就像在問「我們擁有什麼?」而不是「我們可以獲得什麼?」立即的反駁將會是「我們需要什麼?」我說是映射 (Maps)。
Frames 確實啟發並影響了映射 (Maps)。在許多方面,映射 (Maps) 也涵蓋了 Frames,但映射 (Maps) 試圖做更多的事情。最終,我相信它們是兩個不同的事物,並且有不同的目標。
此 EEP 建議為 Erlang 新增一種內建資料類型,即映射 (map),#{ Key => Value }
。
新的資料類型應具有語意、語法和操作,這些語意、語法和操作:
其他語言中存在類似的資料類型,例如 perl 雜湊、ruby 雜湊、python 字典或 scala 映射。
映射 (map) M
由多個關聯組成,並保留從鍵 Term K1..Kn
到值 Term V1..Vn
的關聯,其中沒有兩個鍵匹配。任何 Term,複合或其他,都是可行的鍵或值。類型為 Map 的 Term 可透過 guard 測試 erlang:is_map/1
來識別。沒有對映射 (maps) 進行操作的運算符。在映射 (maps) 內,有兩個中綴運算符。關聯運算符 =>
將鍵與值配對,用於建立和更新。設定值運算符 :=
用於更新現有且匹配的鍵上的值。設定值運算符也用於匹配,以從鍵獲取關聯的值。
映射 (map) 的大小是其集合中關聯的數量。
關聯是映射 (Map) 中鍵 K 到值 V 的鍵值對。
如果 true = K1 =:= K2
,則兩個鍵 K1
和 K2
是匹配的。
為宣告、更新和匹配映射 (maps) 定義的語法。
建構新的映射 (map) 是透過讓運算式 K
與另一個運算式 V
相關聯來完成的
#{ K => V }
新的映射 (maps) 可以透過列出每個關聯來在建構時包含多個關聯
#{ K1 => V1, .. Kn => Vn }
空映射 (map) 是透過不將任何 Term 相互關聯來建構的
#{}
映射 (map) 中的所有鍵和值都是 Term。任何運算式都會先進行評估,然後將產生的 Term 分別用作鍵和值。
鍵和值以 =>
箭頭分隔,關聯以 ,
分隔。
範例
M0 = #{}, % empty map
M1 = #{ a => <<"hello">> }, % single association with literals
M2 = #{ 1 => 2, b => b }, % multiple associations with literals
M3 = #{ A => B }, % single association with variables
M4 = #{ {A, B} => f() }. % compound key associated to an evaluated expression
其中,A
和 B
是任何運算式,而 M0
到 M4
是產生的映射 (map) Term。
如果宣告了兩個匹配的鍵,則後一個鍵將優先。
範例
1> #{ 1 => a, 1 => b }.
#{ 1 => b }
2> #{ 1.0 => a, 1 => b }.
#{ 1 => b, 1.0 => a }
評估建構鍵及其關聯值的運算式順序未定義。建構中鍵值對的語法順序無關緊要,除非在上述兩個匹配鍵的情況下。
以下是建構的簡單 BNF 文法
<map-construct> ::= '#{' <key-value-exprs> '}'
<key-value-exprs> ::= /* empty */
| <key-value-list>
<key-value-list> ::= <key-value-assoc>
| <key-value-assoc> ',' <key-value-list>
<key-value-assoc> ::= <expr> '=>' <expr>
<expr> ::= <Erlang-expression>
更新映射 (map) 的語法與建構語法類似。
定義要更新的映射 (map) 的運算式放在定義要更新的鍵及其各自的值的運算式前面。
M#{ K => V }
其中 M
是類型為映射 (map) 的 Term,而 K
和 V
是任何運算式。
如果鍵 K
與映射 (map) 中任何現有的鍵不匹配,則將建立一個從鍵 K
到值 V
的新關聯。如果鍵 K
與映射 (map) M
中的現有鍵匹配,則其關聯的值將被新值 V
取代。在這兩種情況下,評估的映射 (map) 運算式都將傳回新的映射 (map)。
如果 M
不是映射 (map) 類型,則會拋出 badmap
類型的例外狀況。
若僅更新現有值,請使用以下語法:
M#{ K := V }
其中 M
是類型為映射 (map) 的 Term,V
是一個運算式,而 K
是一個評估為 M
中現有鍵的運算式。
如果鍵 K
與映射 (map) M
中的任何現有鍵不匹配,則會在執行時觸發 badarg
類型的例外狀況。如果映射 (map) M
中存在匹配的鍵 K
,則其關聯的值將被新值 V
取代,並且評估的映射 (map) 運算式會傳回新的映射 (map)。
如果 M
不是映射 (map) 類型,則會拋出 badmap
類型的例外狀況。
範例
M0 = #{},
M1 = M0#{ a => 0 },
M2 = M1#{ a => 1, b => 2 },
M3 = M2#{ "function" => fun() -> f() end },
M4 = M3#{ a := 2, b := 3 }. % 'a' and 'b' was added in `M1` and `M2`.
其中 M0
是任何映射 (map)。由此可知,M1 .. M4
也是映射 (maps)。
更多範例
1> M = #{ 1 => a }.
#{ 1 => a }
2> M#{ 1.0 => b }.
#{ 1 => a, 1.0 => b }.
3> M#{ 1 := b }.
#{ 1 => b }
4> M#{ 1.0 := b }.
** exception error: bad argument
與建構中一樣,評估鍵和值運算式的順序未定義。更新中鍵值對的語法順序無關緊要,除非在兩個鍵匹配的情況下,在這種情況下會使用後一個值。
以下是映射 (map) 更新的簡單 BNF 文法
<map-construct> ::= <map-expr> '#{' <key-value-exprs> '}'
<key-value-exprs> ::= /* empty */
| <key-value-list>
<key-value-list> ::= <key-value>
| <key-value> ',' <key-value-list>
<key-value> ::= <key-value-assoc>
| <key-value-exact>
<key-value-assoc> ::= <expr> '=>' <expr>
<key-value-exact> ::= <expr> ':=' <expr>
<map-expr> ::= <Erlang expression evaluating to a term of type map>
<expr> ::= <Erlang expression>
為了存取映射 (maps) 中的單一值,我們使用一個反關聯
V = M#{ K }.
其中 M
是映射 (Map),而 K
是任何 Term。
如果鍵 K
與映射 (map) M
中的現有鍵匹配,則關聯的值將繫結到 V
。如果鍵 K
與映射 (map) M
中的任何現有鍵不匹配,則在執行時會發生 badarg
例外狀況。
範例
M1 = #{ a => 1, c => 3 },
3 = M1#{ c }.
M2 = #{ 1.0 => a },
a = M2#{ 1 }.
從映射 (maps) 匹配鍵值關聯,並以匹配運算符為例,以以下方式完成
#{ K := V } = M
其中 M
是任何映射 (map)。鍵 K
必須是一個帶有繫結變數或常值的運算式,而 V
可以是任何帶有繫結或未繫結變數的模式。如果 V
中的變數未繫結,則它將繫結到與鍵 K
關聯的值,該值必須存在於映射 (map) M
中。如果 V
中的變數已繫結,則它必須與 M
中與 K
關聯的值匹配。
範例
M = #{ a => {1,2}},
#{ a := {1,B}} = M.
這將把變數 B
繫結到整數 2
。
同樣,可以匹配映射 (map) 中的多個值
#{ K1 := V1, .., Kn := Vn } = M
其中鍵 K1 .. Kn
是任何帶有常值或繫結變數的運算式。如果所有鍵都存在於映射 (map) M
中,則 V1 .. Vn
中的所有變數都將與各自鍵的關聯值匹配。
如果匹配條件不符,則匹配將失敗,並出現以下情況:
badmatch
例外狀況,映射 (maps) 中的匹配只允許使用 :=
作為關聯的分隔符。
在匹配中宣告鍵的順序無關緊要。
在匹配中允許重複的鍵,並將匹配與鍵相關聯的每個模式。
#{ K := V1, K := V2 } = M
將運算式與空的映射 (map) 常值進行匹配將匹配其類型,但不繫結任何變數
#{} = Expr
如果運算式 Expr
的類型為映射 (map),則此運算式將匹配,否則將因 badmatch
例外狀況而失敗。
匹配語法的文法與建構語法的文法相似。
函數標頭中允許將常值作為鍵進行匹配。
%% only start if not_started
handle_call(start, From, #{ state := not_started } = S) ->
...
{reply, ok, S#{ state := start }};
%% only change if started
handle_call(change, From, #{ state := start } = S) ->
...
{reply, ok, S#{ state := changed }};
更多匹配語法,計算列表中 Term 的頻率。
freq(Is) -> freq(Is, #{}).
freq([I|Is], #{I := C} = M) -> freq(Is, M#{ I := C + 1});
freq([I|Is], M) -> freq(Is, M#{ I => 1 });
freq([], M) -> maps:to_list(M).
用於比較的 gb_trees
的等效程式碼
freq(Is) -> freq(Is, gb_trees:empty()).
freq([I|Is], T) ->
case gb_trees:lookup(I, T) of
none -> freq(Is, gb_trees:enter(I, 1), T);
{value, V} -> freq(Is, gb_trees:enter(I, V + 1, T))
end;
freq([], T) -> gb_trees:to_list(T).
可以改進舊的 API 以使用映射 (map) 語法
1> {ok, #{ type := Type, mtime := Mtime }} = file:read_file_info(File).
2> io:format("type: ~p, mtime: ~p~n", [Type, Mtime]).
type: regular, mtime: {{2012,7,18},{19,59,18}}
ok
3>
映射 (map) 推導式宣告
M1 = #{ E0 => E1 || K := V <- M0 }
其中 M0
是任何映射 (Map),E0
和 E1
是任何 erlang 運算式,K
和 V
構成要由 M0
中每個關聯匹配的模式。
對於產生器中的每個序列,都會從評估的運算式 E0
建立到評估的運算式 E1
的關聯。
如果 M0
不是映射 (Map),則會產生類型為 {bad_generator, M0}
的執行時例外狀況。
以下是映射 (map) 推導式的簡單 BNF 文法
<comprehension> ::= '#{' <key-value-assoc> '||' <comprehension-exprs> '}'
<comprehension-exprs> ::= <comprehension-expr>
| <comprehension-exprs> ',' <comprehension-expr>
<comprehension-expr> ::= <generator>
| <filter>
<generator> ::= <key-value-exact> '<-' <expr>
<filter> ::= <expr>
<key-value-assoc> ::= <expr> '=>' <expr>
<key-value-exact> ::= <expr> ':=' <expr>
<expr> ::= <Erlang expression>
來自所有產生器的每個關聯(滿足篩選器)都具有一個環境,該環境由初始環境和關聯的環境組成。
範例
M0 = #{ K => V*2 || K := V <- map() },
M1 = #{ I => f(I) || I <- list() },
M2 = #{ K => V || <<L:8,K:L/binary,V/float>> <= binary() }.
映射 (map) 產生器也可以在二進制和列表推導式中使用。
範例
B1 = << <<V:8>> || _ := V <- map() >>,
L1 = [ {K,V} || K := V <- map() ].
可以為映射 (map) 直接且唯一地指定事先知道的鍵。
-spec func(Opt, M) -> #{ 'status' => S, 'c' => integer() } when
Opt :: 'inc' | 'dec',
M :: #{ 'status' => S, 'c' => integer() },
S :: 'update' | 'keep'.
func(inc, #{ status := update, c := C} = M) -> M#{ c := C + 1};
func(dec, #{ status := update, c := C} = M) -> M#{ c := C - 1};
func(_, #{ status := keep } = M) -> M.
也可以僅透過類型指定。
-spec plist_to_map(Ls) -> #{ binary() => integer() } when
Ls :: [{binary(), integer()}].
plist_to_map([], M) ->
M;
plist_to_map([{K,V}|Ls], M) when is_binary(K), is_integer(V) ->
plist_to_map(Ls, M#{ K => V });
plist_to_map([_|Ls], M) ->
plist_to_map(Ls, M).
同樣,可以將其指定為類型。
-type map1() :: #{ binary() => integer() }.
-type map2() :: #{ <<"list1">> | <<"list2">> => [numbers()] }.
實作函數的模組目前以複數命名,maps
與 lists
、gb_trees
、sets
等相同,但是單數 map
更短,可能更可取。
映射 (maps) 的函數和語意。最初的許多靈感來自 Richard O’Keefe 的 frames 提案。
erlang:is_map(M :: term()) -> boolean().
#此函式為保護函式 (guard function)。
語法等效:try #{} = M, true catch error:{badmatch,_} -> false end
。
如果 M 為映射 (map),則此函式回傳 true
,否則回傳 false
。
erlang:map_size(M :: map()) -> non_neg_integer().
#此函式為保護函式 (guard function)。
語法等效:無。
此函式回傳映射中鍵值對的數量。此操作時間複雜度為常數。
與 maps:size(M)
相同。
maps:remove(K0 :: term(), M0 :: map()) -> M1 :: map().
#語法等效:#{ K => V || K := V <- M0, K =/= K0 }
。 僅限列表推導式 (comprehensions)
此函式從映射 M0
中移除鍵 K0
(如果存在) 及其關聯的值,並回傳一個不含鍵 K0
的新映射 M1
。
與 maps:from_list([{K,V}||{K,V} <- maps:to_list(M0), K =/= K0])
相同。
maps:get(K :: term(), M :: map()) -> V :: term().
#語法等效:M#{ K }
。
如果映射 M
包含一個符合 K
的鍵,則回傳與鍵 K
相關聯的值 V
。如果沒有值與鍵 K
相關聯,則呼叫將會失敗並產生例外。
maps:keys(M :: map()) -> [K1, .., Kn].
#語法等效:[K || K := _ <- M]
。
回傳映射 M
中所有鍵的完整列表,順序任意。
與 [K || {K,_} <- maps:to_list(M)]
相同。
maps:find(K :: term(), M :: map()) -> {ok, V :: term()} | error.
#語法等效:try #{ K := V } = M, V catch error:{badmatch,_} -> error end
。
如果映射 M
包含鍵 K
,則回傳包含與鍵 K
相關聯的值 V
的元組 {ok, V}
。如果沒有值與鍵 K
相關聯,則此函式回傳 error
。
maps:fold(F :: fun((K :: term(), V :: term(), In :: term()) -> Out :: term()), A :: term(), M :: map()) -> Result :: term().
#語法等效:無
針對映射 M
中每個鍵 K
與值 V
的關聯,以任意順序呼叫 F(K, V, In)
。函式 fun F/3
必須回傳一個新的累積器,該累積器會傳遞至下一個連續呼叫。maps:fold/3
回傳累積器的最終值。如果映射為空,則回傳初始累積器值 A
。
與 lists:foldl(fun({K,V}, In) -> F(K,V,In) end, A, maps:to_list(M))
相同。
maps:from_list([{K1,V1}, .., {Kn,Vn}]) -> M :: map().
#語法等效:#{ K1 => V1, .., Kn => Vn }
此函式接收鍵值組元組的列表,並建立一個映射。關聯可以依任何順序排列,而且關聯中的鍵和值都可以是任何類型。如果同一個鍵出現多次,則使用最後一個 (最右邊) 的值,並忽略之前的值。
maps:is_key(K :: term(), M :: map()) -> bool().
#語法等效:try #{ K := _ } = M, true catch error:{badmatch, _} -> false end
。
如果映射 M
包含一個符合 K
的鍵,則回傳 true
。
maps:map(F :: function(), M0 :: map()) -> M1 :: map().
#語法等效:#{ K => F(K,V) || K := V <- M }
。 僅限列表推導式
此函式透過針對映射 M0
中每個鍵 K
與值 V
的關聯,以任意順序呼叫函式 fun F(K, V)
,來產生一個新的映射 M1
。函式 fun F/2
必須回傳要與新映射 M1
的鍵 K
相關聯的值。
與 maps:from_list(lists:map(fun({K,V}) -> {K, F(K,V)} end, maps:to_list(M)))
相同。
maps:new() -> M :: map().
#語法等效:#{}
。
回傳一個新的空映射。
與 maps:from_list([])
相同。
maps:size(M :: map()) -> Size :: non_neg_integer().
#語法等效:無。
此函式回傳映射中鍵值關聯的數量。此操作時間複雜度為常數。
與 erlang:map_size(M)
相同。
maps:put(K :: term(), V :: term(), M0 :: map()) -> M1 :: map().
#語法等效:M0#{ K => V }
。
將鍵 K
與值 V
關聯,並將此關聯插入映射 M0
。如果存在一個符合 K
的鍵,則舊的關聯值會被值 V
取代。此函式回傳一個包含新關聯的新映射 M1
。
與 maps:from_list(maps:to_list(M0) ++ [{K,V}])
相同。
maps:to_list(M :: map()) -> [{K1,V1}, ..., {Kn,Vn}].
#語法等效:[{K, V} || K := V <- M]
。
其中,會以任意順序回傳配對 [{K1,V1}, ..., {Kn,Vn}]
。
maps:update(K :: term(), V :: term, M0 :: map()) -> M1 :: map()
#語法等效:M0#{ K := V }
。
如果存在一個符合 K
的鍵,則舊的關聯值會被值 V
取代。此函式回傳一個包含新關聯值的新映射 M1
。
與 maps:from_list(maps:to_list(M0) ++ [{K,V}])
相同。
maps:values(M :: map()) -> [V1, .., Vn].
#語法等效:[V || _ := V <- M]
。
回傳映射 M
中包含的所有值的完整列表,順序任意。
與 [V || {_,V} <- maps:to_list(M)]
相同。
maps:without([K1, .., Kn] = Ks, M0 :: map()) -> M1 :: map().
#語法等效:#{ K => V || K := V <- M0, not lists:member(K, Ks) }
。 僅限列表推導式
從映射 M0
中移除鍵 K1
到 Kn
及其關聯的值,並回傳一個新的映射 M1
。
與 maps:from_list([{K,V}||{K,V} <- maps:to_list(M0), not lists:member(K, Ks)])
相同。
maps:merge(M0 :: map(), M1 :: map()) -> M2 :: map().
#語法等效:無
將兩個映射合併為一個單一映射。如果兩個映射中存在兩個符合的鍵,則映射 M0
中的值將會被映射 M1
中的值取代。
如果 term A
和 term B
都是映射,則
A
和 B
的大小不同,則 A
和 B
不相等。A
和 B
的所有對應鍵與其對應值都成對相等,則 A
和 B
相等。A
和 B
不相等。因此,兩個映射相等的條件是,它們必須具有相同的類型、大小,而且所有對應的鍵值關聯都成對相等。
term 的順序定義在 Erlang 規格 4.7 中,並引述如下
term 主要依其類型排序,順序如下
numbers < atoms < refs < ports < PIDs < tuples < empty list < conses < binary
此處的規格不完整,實際 term 順序為
numbers < atoms < refs < funs < ports < pids < tuples < empty list < conses < binaries
映射資料類型在元組之後排序。
numbers < atoms < refs < funs < ports < pids < tuples < maps < empty list < conses < binaries
----
映射首先依其大小排序,然後依其各自的鍵排序,最後依 Erlang term 順序的關聯值排序。
給定兩個大小相同的映射 M1
和 M2
,它們會進行比較,使得 M1
中每個依 Erlang term 順序的鍵,都會與 M2
的對應鍵比較。先比較所有的鍵,再比較值,直到找到差異為止。如果鍵或值不同,則各自 term 的順序 (依 Erlang term 順序) 即為映射的順序。如果鍵值組都相同,則認為映射相等。
範例
> #{ b => 2 } > #{ a => 2 }. % b > a
true
> #{ b => 2 } > #{ a => 1, b => 2 }. % size 1 < size 2
false
> #{ b => 1 } > #{ b => 2}. % 1 < 2
false
> #{ b => 2, c => 3 } > #{ b => 1, d => 3}. % c > d, compared before 2 and 1
false
> #{ b => 1 } > #{ 1 => 1}. % b > 1
true
> #{ 1.0 => a } == #{ 1 => a }. % 1.0 == 1
true
> #{ 1.0 => a } =:= #{ 1 => a }. % 1.0 =:= 1
false
映射會以任意順序印出鍵。
映射關聯運算子和設定值運算子會最後排序,在符合運算子和 catch
之後。
:
#
Unary + - bnot not
/ * div rem band and Left associative
+ - bor bxor bsl bsr or xor Left associative
++ -- Right associative
== /= =< < >= > =:= =/=
andalso
orelse
= ! Right associative
catch
=> :=
因此,映射表達式
#{ key => C = 1 + 2 }
會依以下順序求值
#{ key => ( C = ( 1 + 2 ) ) }
模式匹配是非常強大的 Erlang 工具。映射在其模式匹配中引入了一些新功能。
我們將使用符合運算子來舉例說明。
表面上,使用映射的模式匹配與記錄相似。LHS 模式中要求的鍵將會與 RHS 關聯映射中找到的值繫結。
1> #{ a := V } = #{ a => 1 }.
#{ a => 1 }
2> 1 = V.
1
在 RHS 映射中找不到 LHS 模式中要求的鍵,將會產生例外,exception error: no match of right hand side value ...
。
1> #{ not_in_map := V } = #{ a => 1 }.
** exception error: no match of right hand side value #{ a => 1 }
同樣地,如果 LHS 模式中要求的鍵的值與 RHS 映射中關聯鍵的值不符合,則符合作業將會產生例外。
1> #{ a := 10 } = #{ a => 1 }.
** exception error: no match of right hand side value #{ a => 1 }
只有要求的鍵才會繫結至關聯。符合作業中映射內存在的任何未要求的鍵都會被忽略。
1> #{ a := V1, b := V2 } = #{ a => 1, b => 2, c => 3}.
#{ a => 1, b => 2, c => 3 }
2> 1 = V1.
1
3> 2 = V2.
2
在模式匹配映射時,要求的鍵的順序沒有意義。
1> #{ a := "1", b := "2" } = #{ a => "1", b => "2" }.
#{ a => "1", b => "2" }
2> #{ b := "2", a := "1" } = #{ a => "1", b => "2" }.
#{ a => "1", b => "2" }
以下的範例是建構的範例,用來示範映射模式匹配的強大功能。
符合表達式的求值方式是,表達式中用作鍵的變數會在 (如果可能) 求值之前進行繫結。
例如,鍵可以透過其他鍵值關聯進行繫結。
1> #{ K := V, id := K } = M = #{ id => b, a => 1, b => 2, c => 3}.
#{ id => b, a => 1, b => 2, c => 3}
2> b = K.
b
3> 2 = V.
2
在此情況下,先求值已繫結的鍵 id
,並在 M 中查閱,將變數 K
繫結。然後可以使用繫結至 b
的 K
將 V
繫結至 2。
繫結用作鍵的變數要求必須有一個沒有循環的繫結順序。重新排序會擴展到符合表達式中的所有 term,因此
1> { #{ X := Y }, X } = { #{ 5 => 10 }, 5 }.
在 X
和 Y
未繫結的情況下,會產生成功的符合,將 X
繫結至 5,將 Y
繫結至 10。
這在更新映射關聯中的特定項目時特別有用
%% Function declared in module map_example
update_values([{K, V1}|Ls], #{ K := V0 } = M) -> update_values(Ls, M#{ K := V0 + V1 });
update_values([_|Ls], M) -> update_values(Ls, M);
update_values([], M) -> M.
第一個函式子句在這裡很重要。鍵 K
會在元組中繫結,並將用來從映射 M
中要求值 V0
。然後更新映射 M
,將鍵 K
與新值 V0 + V1
關聯。
%% In the Erlang shell
1> M = #{ "a" => 1, "b" => 2, "c" => 3 }.
#{ "a" => 1, "b" => 2, "c" => 3 }
2> map_example:update_values([{"b", 10}, {"c", 20}, {"d", 30 }], M).
#{ "a" => 1, "b" => 12, "c" => 23 }
請注意,由於鍵 "d"
不存在於映射 M
中,因此它無法與第一個子句符合,而且不會使用關聯 "d" => 40
來更新映射。
如果符合的 LHS 中存在循環相依性的表達式,例如
1> #{X := Y, Y := X} = #{5 => 10, 10 => 5}.
將會產生求值器錯誤 (變數未繫結) 或編譯錯誤。
有 255 個標籤可用於編碼外部二進位發佈的 term。所有標籤都定義在 external.h
中。編碼以魔術位元組 131
開頭。
R15B01 中使用的編碼標籤如下
SMALL_INTEGER_EXT 'a' 97
INTEGER_EXT 'b' 98
FLOAT_EXT 'c' 99
ATOM_EXT 'd' 100
SMALL_ATOM_EXT 's' 115
REFERENCE_EXT 'e' 101
NEW_REFERENCE_EXT 'r' 114
PORT_EXT 'f' 102
NEW_FLOAT_EXT 'F' 70
PID_EXT 'g' 103
SMALL_TUPLE_EXT 'h' 104
LARGE_TUPLE_EXT 'i' 105
NIL_EXT 'j' 106
STRING_EXT 'k' 107
LIST_EXT 'l' 108
BINARY_EXT 'm' 109
BIT_BINARY_EXT 'M' 77
SMALL_BIG_EXT 'n' 110
LARGE_BIG_EXT 'o' 111
NEW_FUN_EXT 'p' 112
EXPORT_EXT 'q' 113
FUN_EXT 'u' 117
DIST_HEADER 'D' 68
ATOM_CACHE_REF 'R' 82
ATOM_INTERNAL_REF2 'I' 73
ATOM_INTERNAL_REF3 'K' 75
BINARY_INTERNAL_REF 'J' 74
BIT_BINARY_INTERNAL_REF 'L' 76
COMPRESSED 'P' 80
對於映射,我們將標籤 MAP_EXT
定義為 116 (t
)。
資料配置
MAP_EXT
|-----------------------------------
| 1 | 4 | | |
|-----------------------------------
| 116 | Size | Keys | Values |
|-----------------------------------
Size
指定大小描述元之後的鍵和值的數量。
未解決的問題,針對以下項目進行最佳化
記憶體大小應為優先考量,因為我們透過網路傳輸這些資料。我們應該促進易用性,以便其他語言可以整合到此格式。
這導致了扁平且簡單的結構。因此,編碼/解碼會產生效能上的損失。
當我們已經有記錄 (records)、字典 (dicts)、gb_trees、ets 和 proplists 時,為什麼我們還需要 Map?
Map 被設想為一個易於使用、輕量但功能強大的鍵值關聯儲存。
Map 利用了 Erlang 的主要優勢之一,模式匹配,以豐富使用者體驗並提供一個強大的工具來簡化程式碼開發。在易用性方面,模式匹配使 Map 明顯優於 dict、gb_trees 或 proplists。
Map 提供將任意術語作為鍵(不僅限於原子),並將任意術語作為值,關聯在一個能夠匹配的資料類型中的可能性。
Map 並不聲稱要像 Frames 提案那樣取代記錄 (records)。相反,Map 的目標是更大的使用領域,並希望成為記錄的補充,並在適當的情況下取代它們。
Map 最初並不是被設想為記錄的替代品,這是一個後來加入的期望要求。記錄替代方法不一定會限制任何語義,但它可能會對實作和底層結構產生一些約束。
在適當的情況下,記錄功能強大。
然而,一些缺點是
當比較 Map 和記錄時,Map 很容易彌補這些缺點,但是,在內建資料類型中,正向的效應並不容易複製,在內建資料類型中,值是在執行時而不是在編譯時確定的。
已經提出了使用 Frames 或 Map 的更簡單 JSON 表示形式的論點。使用 Frames 並因此使用原子來動態建立鍵將是一個嚴重的缺點。使用 Map 將允許使用字串二進位制來表示鍵,並且不會使全域原子池混亂。
變更 json 解碼。Mochiwebs mochijson 將 Json 字典解碼為以下內容
{"key": "value"} -> {struct, [{"key", "value"}]}
這可以改為
{"key": "value"} -> #{ "key" => "value"}
考慮以下來自 json.org 的 JSON 範例。
{"menu": {
"id": "file",
"value": "File",
"popup": {
"menuitem": [
{"value": "New", "onclick": "CreateNewDoc()"},
{"value": "Open", "onclick": "OpenDoc()"},
{"value": "Close", "onclick": "CloseDoc()"}
]
}
}}
mochijson:decode/1
目前看起來會像
{struct, [
{"menu", {struct, [
{"id","file"},
{"value","File"},
{"popup", {struct, [
{"menuitem", {array, [
{struct, [{"value","New"},{"onclick","CreateNewDoc()"}]},
{struct, [{"value","Open"},{"onclick","OpenDoc()"}]},
{struct, [{"value","Close"}, {"onclick","CloseDoc()"}]}
]}}
]}}
]}}
]}
mochijson:decode/1
可以看起來像
#{ "menu" => #{
"id" => "file",
"value" => "File",
"popup" => #{
"menuitem" => [
#{ "value" => "New", "onclick" => "CreateNewDoc()"},
#{ "value" => "Open", "onclick" => "OpenDoc()"},
#{ "value" => "Close", "onclick" => "CloseDoc()"}
]
}
}}
讓我們找到 "menu"
-> "popup"
-> "menuitem"
。
遍歷第一個結構有點笨拙。我們必須執行以下操作
Decoded = mochijson:decode(Json),
{struct, Menu} = proplists:get_value("menu", Decoded),
{struct, PopUp} = proplists:get_value("popup", Menu),
{struct, Items} = proplists:get_value("menuitem", PopUp),
使用 Map,它可能看起來像這樣
#{ "menu" := Menu } = mochijson:decode(Json),
#{ "popup" := PopUp } = Menu,
#{ "menuitem" := Items } = PopUp,
甚至
Decoded = mochijson:decode(Json),
#{ "menu" := #{ "popup" := #{ "menuitem" := Items } } } = Decoded,
使用 Map 和單個值存取,它可能看起來非常簡單
Decoded = mochijson:decode(Json),
Items = Decoded#{ "menu" }#{ "popup" }#{ "menuitem "}.
我們有一些使用情境仍在討論中。每個問題都會給出一個建議的答案,這些答案來自本提案中的討論。
behavior
,而實際上想要的是鍵 behaviour
。此時會出現執行時例外,而不是獲得兩個不同的鍵。這些問題的答案對於我們應如何設計和實作 Map 至關重要。我們還應該記住的是,我們永遠不會完全擺脫記錄,因此 Frames 的一些論點可能是多餘的。
如前所述,目前的語法尚未確定,但我們應該對它有什麼期望?
首先,它必須是明確的,這表示語法必須產生人類和機器都無法誤解的單一明確定義的行為。
1.1 這裡我們還包括了類似語法應該具有類似行為的概念,或者至少不應該是完全不同的行為。例如,記錄 #record{ key = Value }
,具有 O(1) 的效能和 2 + N 個字的記憶體開銷。如果我們使用類似的語法,例如 #{ key = Value }
,對於 Map 的任何大小,我們也應該期望在語義和效能方面都有類似的行為。
語法必須盡可能簡短。只要不違反規則 1,就不能使用比描述特定行為所需的更多字元。
2.2 我們希望避免語言中的冗長。冗長會將資訊從我們的視野中推開並使之模糊。這需要避免。
作者主張: =>
作為「設定或更新」中的分隔符號,而 :=
用於「設定現有」和「匹配」語法。
在下面的範例中,我們使用 #{ Key => Value }
來描述 Map 語義和使用案例,但這只是眾多建議中的一種。
存在多種語法提案,Frames 提案使用 <{ key ~ Value }>
語法,而另一個語法建議與記錄語法 #{ key = Value }
非常相似。
目前的變數和原子定義限制了我們可以用作分隔符號的內容,而 ~
和 =
是我們可以使用的唯一合理的單個字元分隔符號。Richard O'Keefe 在他的 不再需要記錄(第五版) 中充分論證了這一點。
反對使用 =
分隔符號的論點是
#{ A = B = v }
,A
是否匹配 B
,或者 B
是否匹配 v
?哪個 =
是匹配運算,哪個 =
分隔鍵值對?Key
「等於」Value
。因此,=
違反了我們對語法有什麼期望?中的規則 #1。對此語法的解釋是含糊不清的。
反對使用 ~
分隔符號的論點是
Key
「非」Value
,因為 ~
是 C 中的按位非運算子。Key
「大約等於」Value
,因為 ~
類似於數學中的 ≃
。-
的區別,例如。K ~ V
可能會被解釋為 K - V
,考慮 #{ K - 1 ~ 2 - V }
。因此,這違反了我們對語法有什麼期望?中的規則 #1。對此語法的解釋是含糊不清的。
兩個雙字元分隔符號建議是 #{ Key := Value }
和 #{ Key => Value}
,其中 :=
是指派的通用分隔符號,而 =>
應該讀作映射到。如果可以的話,應該避免使用雙字元分隔符號,因為它會增加原始碼的語法佔用空間。
指派分隔符號對於指派本身來說讀起來很好,但在模式匹配方面,它也與記錄一樣存在邏輯反轉的缺陷。匹配 #{ key := Value } = M
讀作將 M 匹配到 Map 模式,其中 Value 等於 key。除非我們將指派分隔符號 :=
稱為與它本身無關的東西,否則讀起來並不順暢。
然而,:=
也類似於 =:=
,後者表示「完全相等」,也就是匹配。這是一個有價值的含義,因為在處理數字時,==
和 =:=
之間存在差異,因此 :=
可以成為匹配語法中更正確的分隔符。
如果 ->
這個分隔符不是因為會重載函數子句的含義,那它會是一個合適的選擇。
當處理二元資料時,->
和 =>
都可能會令人困惑。考慮 #{ <<"key">> -> <<"value">> }
和 #{ <<"key">> => <<"value">> }
,其中 =>
似乎比 ->
更令人困惑一些。
以下是從上述感知到的理想程度排列的分隔符列表
#{ K => V }
- 沒有歧義,沒有重載,讀起來像是一個關聯#{ K := V }
- 沒有歧義,沒有重載,讀起來像是賦值或精確匹配#{ K ~ V }
- 排版上的歧義,沒有重載,含義不明確#{ K -> V }
- 重載了函數子句的頭部和主體分隔符,讀起來像是一個關聯#{ K = V }
- 重載了匹配運算符,讀起來像是匹配或賦值對於現有鍵使用 :=
賦值似乎是一個不錯的選擇。對於設定或更新的選擇,則是在 =>
和 ~
之間。
這裡提出了一種在 Map 中更新值的兩種不同方式。
一種語法是,當且僅當我們想更新一個已經*存在*的鍵的值時使用,另一種是當我們想要更新 Map 中任何鍵時使用。
M#{ K => V }
來宣告新的鍵值對*或*更新已存在的鍵M#{ K := V }
來更新已存在的鍵。#{ K := V } = M
來匹配 Map。範例 1
foo() ->
M = #{ key1 => 1, key2 => 2 }, % M is declared with keys 'key1' and 'key2'
bar(M).
bar(M) ->
M#{
key1 := "1", %% 'key1' will be set to "1"
key2 := "2", %% 'key2' will be set to "2"
key3 := "3" %% this causes an exception since 'key3' does not exist in M
}.
> foo().
** exception error: no match of 'key3' in map
範例 2
foo() ->
M = #{ key1 => 1, key2 => 2 }, % M is declared with keys 'key1' and 'key2'
bar(M).
bar(M) ->
M#{
key1 => "1", %% 'key1' will be set to "1"
key2 => "2", %% 'key2' will be set to "2"
key3 => "3" %% 'key3' will be set to "3"
}.
> foo().
#{ key1 => 1, key2 => "2", key3 => "3" }
我們必須減少語法足跡對原始碼和語言的影響。
目前,將選項傳遞給函數的兩種常見方式是透過記錄或屬性列表。兩者都有一些缺點。記錄是編譯時相依的,並且是元組的語法糖。屬性列表是通用的,但在定義和操作它們時會產生大量的文字。
考慮在解析參數列表時的這個範例
args(Args) ->
args(Args, [{analyze, false}, {suppression, false}, {target, none}]).
args(["-r" | Args], Opts) ->
args(Args, [{analyze, true} | proplists:delete(analyze, Opts)]);
args(["-s="++File | Args], Opts) ->
args(Args, [{suppression, File} | proplists:delete(suppression, Opts)]);
args([Target], Opts) ->
[{target, Target} | proplists:delete(target, Opts)].
當操作屬性列表時,文字的影響(字元數)非常大。
如果我們改用某種具有語法的 Map,那會是什麼樣子呢?
args(Args) ->
args(Args, #{ analyze => false, suppression => false, target => none}).
args(["-r" | Args], Opts) -> args(Args, Opts#{ analyze := true });
args(["-s="++File | Args], Opts) -> args(Args, Opts#{ suppression := File});
args([Target], Opts) -> Opts#{ target := Target }.
我認為這樣看起來更簡潔,但這是一個非常主觀的看法。為了使用一些數據,我們可以計算字元數,我們看到屬性列表範例有 390 個字元,而 Map 範例有 306 個字元。在這個範例中,屬性列表使用了將近 30% 更多的字元。
也許最合理的 maps:from_list/1
語義是讓鍵值的重要性順序從左到右,這表示使用第一個關聯,而忽略具有匹配鍵的後續值。
這與 dict:from_list/1
的行為不同。
考慮以下 dict
範例
[{a,2}] = dict:to_list(dict:from_list([{a,1}, {a,2}])).
透過讓最左邊的鍵成為最重要的鍵,我們可以簡化與列表之間的轉換。
目前的建議具有以下語義
Ls = [{a,old}],
#{ a := old } = maps:from_list([{a,new}|Ls]).
反轉會是
Ls = [{a,old}],
#{ a := new } = maps:from_list([{a,new}|Ls]).
Erlang 規範對實作設定的限制是排序必須是完全的,也就是滿足*反對稱性*、*遞移性*和*完全性*。
M1 =< M2
且 M2 =< M1
則 M1 == M2
,M1 =< M2
且 M2 =< M3
則 M1 =< M3
,M1 =< M2
或 M2 =< M1
(總是可比較的) 其中 M1
、M2
和 M3
是任何 Map 項。只有當我們將浮點數和整數視為型別的聯集(即數字)時,這才在 Erlang 中成立。在 Map 的情況下,true = #{ 1.0 => V } == #{ 1 => V}
。
lists:sort([M1, .., Mn])
M1
< M2
會導致內部排序,但會花費 O( *N1* * lg *N1* + *N2* * lg *N2* ),其中 N1 = maps:size(M1) and N2 = maps:size(M2)
我們是否需要單一存取,或者匹配是否足夠?
考慮以下情況,
V = M#{ K }
比
#{ K := V } = M
它還允許輕鬆存取深層結構中的關聯值。
單一值存取的語法是此提案中最不完善(和考慮過的)的功能,肯定可以使用一些輸入。
此外,點語法必須廢除。目前它用於記錄,但不會用於 Map。點表示最後一個子句中表示式列表的結束,或屬性的結束。
它不能用於區分浮點數或關聯。
範例
1> M = #{ 1.1 => a, 1 => #{ 1 => b } }.
#{ 1 => #{ 1 => b }, 1.1 => a }.
2> #M.1.1.
a | b ?
用 Map 編寫的 Erlang 程式碼只能在 Erlang/OTP R17A 及更高版本的 Erlang/OTP 上解析、載入和執行,而不能在先前的版本上執行。
在 Erlang/OTP R17A 之前編寫的 Erlang 程式碼將完全相容,也就是可以透過這些 Map 變更進行解析、載入和執行。
分發將不向下相容。
本文檔已置於公有領域。