映射以及本 EEP 的历程漫长,绝非一帆风顺。大约两三年前,我们首次在 OTP 内部讨论映射时,我对映射的期望有一个清晰的蓝图。本 EEP 与最初的设想类似,但它也吸收了来自 OTP 内外的许多其他想法。
最初的想法是一种数据类型,一种语法感知的键值关联映射,并带有模式匹配。其语法类似于记录,但没有编译时依赖的麻烦,并且可以使用任意项作为键。顺序并不重要,并且可以使用具有良好性能和内存权衡的哈希数组映射树来实现。这是一种不同于替换记录的方法。它的目的是在合适的地方替换记录,在其他方面不是替代品,而是它自己的东西。
来自社区的许多人希望拥有类似映射的数据类型,并提出了一些建议。其中一个突出的建议当然是来自 Richard O'Keefe 的 Frames 提案。它是我见过的最完整的提案,并且考虑得非常周到。它的目标是作为记录的替代品,该提案很好地实现了这个目标。
记录的替代品只是一个替代品。这就像问“我们有什么?”而不是“我们能得到什么?”。立即的反驳是“我们需要什么?” 我说映射。
Frames 确实启发并影响了映射。在许多方面,映射也包含了 Frames,但映射试图做更多的事情。最终,我认为它们是两个不同的事物,并且具有不同的目标。
本 EEP 建议为 Erlang 添加一个新的内置数据类型,即映射,#{ Key => Value }
。
新的数据类型应具有以下语义、语法和操作:
其他语言中存在类似的数据类型,例如 perl 哈希、ruby 哈希、python 字典 或 scala 映射。
映射 M
由若干关联组成,并保持从键项 K1..Kn
到值项 V1..Vn
的关联,其中没有两个键匹配。任何项(复合项或其他项)都是可行的键或值。映射类型的项通过 guard 测试 erlang:is_map/1
来识别。没有对映射进行操作的运算符。在映射中,有两个中缀运算符。关联运算符 =>
将键与值配对,用于创建和更新。设置值运算符 :=
用于更新已存在且匹配的键的值。设置值运算符也用于匹配以从键获取关联的值。
映射的大小是其集中关联的数量。
关联是映射中键 K 到值 V 的键值对。
如果 true = K1 =:= K2
,则两个键 K1
和 K2
是匹配的。
用于声明、更新和匹配映射的已定义语法。
构造新映射是通过将表达式 K
与另一个表达式 V
关联来完成的
#{ K => V }
新映射在构造时可以通过列出每个关联来包括多个关联
#{ K1 => V1, .. Kn => Vn }
通过不将任何项相互关联来构造空映射
#{}
映射中的所有键和值都是项。首先对任何表达式进行求值,然后将结果项分别用作键和值。
键和值由 =>
箭头分隔,关联由 ,
分隔。
示例
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
是生成的映射项。
如果声明了两个匹配的键,则后一个键将优先。
示例
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>
更新映射具有与构造它类似的语法。
定义要更新的映射的表达式放在定义要更新的键及其各自值的表达式之前。
M#{ K => V }
其中 M
是映射类型的项,K
和 V
是任何表达式。
如果键 K
与映射中任何现有键都不匹配,则将创建从键 K
到值 V
的新关联。如果键 K
与映射 M
中的现有键匹配,则其关联的值将被新值 V
替换。在这两种情况下,求值的映射表达式都将返回新映射。
如果 M
不是映射类型,则会抛出 badmap
类型的异常。
要仅更新现有值,请使用以下语法,
M#{ K := V }
其中 M
是映射类型的项,V
是一个表达式,K
是一个求值为 M
中现有键的表达式。
如果键 K
与映射 M
中任何现有键都不匹配,则会在运行时触发 badarg
类型的异常。如果匹配的键 K
存在于映射 M
中,则其关联的值将被新值 V
替换,并且求值的映射表达式返回新映射。
如果 M
不是映射类型,则会抛出 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
是任何映射。由此可见,M1 .. M4
也是映射。
更多示例
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
与构造中一样,键和值表达式的求值顺序未定义。更新中键值对的语法顺序无关紧要,除非在两个键匹配的情况下,在这种情况下使用后一个值。
以下是映射更新的简单 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>
对于访问映射中的单个值,让我们使用解关联
V = M#{ K }.
其中 M
是映射,K
是任何项。
如果键 K
与映射 M
中的现有键匹配,则关联的值将绑定到 V
。如果键 K
与映射 M
中的任何现有键都不匹配,则在运行时会发生 badarg
异常。
示例
M1 = #{ a => 1, c => 3 },
3 = M1#{ c }.
M2 = #{ 1.0 => a },
a = M2#{ 1 }.
从映射中匹配键值关联,以匹配运算符为例,按以下方式完成
#{ K := V } = M
其中 M
是任何映射。键 K
必须是具有绑定变量或文字的表达式,V
可以是具有绑定或未绑定变量的任何模式。如果 V
中的变量未绑定,则它将绑定到与键 K
关联的值,该值必须存在于映射 M
中。如果 V
中的变量已绑定,则它必须与 M
中与 K
关联的值匹配。
示例
M = #{ a => {1,2}},
#{ a := {1,B}} = M.
这会将变量 B
绑定到整数 2
。
类似地,可以匹配映射中的多个值
#{ K1 := V1, .., Kn := Vn } = M
其中键 K1 .. Kn
是任何带有文字或绑定变量的表达式。如果所有键都存在于映射 M
中,则 V1 .. Vn
中的所有变量都将与其各自键的关联值匹配。
如果未满足匹配条件,则匹配将失败,结果是:
badmatch
异常,映射中的匹配仅允许将 :=
用作关联的分隔符。
在匹配中声明键的顺序无关紧要。
匹配中允许重复的键,并且将匹配与键关联的每个模式。
#{ K := V1, K := V2 } = M
将表达式与空映射文字进行匹配将匹配其类型,但不会绑定任何变量
#{} = Expr
如果表达式 Expr
的类型为映射,则此表达式将匹配,否则它将因异常 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 }};
更多匹配语法,计算列表中项的频率。
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 可以改进为使用映射语法
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>
映射推导声明
M1 = #{ E0 => E1 || K := V <- M0 }
其中 M0
是任意 Map,E0
和 E1
是任意 Erlang 表达式,K
和 V
构成 M0
中每个关联的匹配模式。
对于生成器中的每个序列,都会创建一个从计算后的表达式 E0
到计算后的表达式 E1
的关联。
如果 M0
不是 Map,则会生成类型为 {bad_generator, M0}
的运行时异常。
下面是映射推导的简单 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() }.
映射生成器也可以用于二进制和列表推导中。
示例
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
更短,可能更理想。
用于 Map 的函数和语义。最初很多灵感来自 Richard O’Keefe 的框架提案。
erlang:is_map(M :: term()) -> boolean().
#此函数是一个保护函数。
语法等价:try #{} = M, true catch error:{badmatch,_} -> false end
。
如果 M 是 map,则该函数返回 true
,否则返回 false
。
erlang:map_size(M :: map()) -> non_neg_integer().
#此函数是一个保护函数。
语法等价:无。
该函数返回 map 中键值对的数量。此操作以恒定时间发生。
与 maps:size(M)
相同。
maps:remove(K0 :: term(), M0 :: map()) -> M1 :: map().
#语法等价:#{ K => V || K := V <- M0, K =/= K0 }
。仅适用于推导
该函数从 map M0
中删除键 K0
(如果存在)及其关联的值,并返回一个不包含键 K0
的新 map M1
。
与 maps:from_list([{K,V}||{K,V} <- maps:to_list(M0), K =/= K0])
相同
maps:get(K :: term(), M :: map()) -> V :: term().
#语法等价:M#{ K }
。
如果 map M
包含与 K
匹配 的键,则返回与键 K
关联的值 V
。如果键 K
没有关联的值,则调用将失败并出现异常。
maps:keys(M :: map()) -> [K1, .., Kn].
#语法等价:[K || K := _ <- M]
。
返回 map 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
。
如果 map M
包含键 K
,则返回一个元组 {ok, V}
,其中值 V
与键 K
关联。如果键 K
没有关联的值,则该函数将返回 error
。
maps:fold(F :: fun((K :: term(), V :: term(), In :: term()) -> Out :: term()), A :: term(), M :: map()) -> Result :: term().
#语法等价:无
对于 map M
中的每个键 K
到值 V
的关联,以任意顺序调用 F(K, V, In)
。函数 fun F/3
必须返回一个新的累加器,该累加器将传递给下一个连续调用。maps:fold/3
返回累加器的最终值。如果 map 为空,则返回初始累加器值 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 }
该函数接受键值元组元素的列表并构建一个 map。关联的顺序可以是任意的,并且关联中的键和值都可以是任何项。如果同一个键出现多次,则使用后者(最右边)的值,并且忽略先前的值。
maps:is_key(K :: term(), M :: map()) -> bool().
#语法等价:try #{ K := _ } = M, true catch error:{badmatch, _} -> false end
。
如果 map M
包含与 K
匹配 的键,则返回 true
。
maps:map(F :: function(), M0 :: map()) -> M1 :: map().
#语法等价:#{ K => F(K,V) || K := V <- M }
。仅适用于推导
该函数通过对 map M0
中的每个键 K
到值 V
的关联,以任意顺序调用函数 fun F(K, V)
来生成新的 map M1
。函数 fun F/2
必须返回与新 map M1
的键 K
关联的值。
与 maps:from_list(lists:map(fun({K,V}) -> {K, F(K,V)} end, maps:to_list(M)))
相同。
maps:new() -> M :: map().
#语法等价:#{}
。
返回一个新的空 map。
与 maps:from_list([])
相同。
maps:size(M :: map()) -> Size :: non_neg_integer().
#语法等价:无。
该函数返回 map 中键值关联的数量。此操作以恒定时间发生。
与 erlang:map_size(M)
相同。
maps:put(K :: term(), V :: term(), M0 :: map()) -> M1 :: map().
#语法等价:M0#{ K => V }
。
将键 K
与值 V
关联,并将关联插入到 map M0
中。如果存在与 K
匹配 的键,则旧的关联值将被值 V
替换。该函数返回一个包含新关联的新 map 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
替换。该函数返回一个包含新关联值的新 map M1
。
与 maps:from_list(maps:to_list(M0) ++ [{K,V}])
相同。
maps:values(M :: map()) -> [V1, .., Vn].
#语法等价:[V || _ := V <- M]
。
返回 map 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) }
。仅适用于推导
从 map M0
中删除键 K1
到 Kn
及其关联的值,并返回一个新的 map 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().
#语法等价:无
将两个 map 合并为一个 map。如果两个 map 中存在两个匹配的键,则 map M0
中的值将被 map M1
中的值取代。
如果项 A
和项 B
都是 map,
A
和 B
的大小不同,则 A
和 B
不相等。A
和 B
的所有对应键都与其对应的值成对相等,则 A
和 B
相等。A
和 B
不相等。因此,当且仅当两个 map 具有相同的类型、大小,并且所有对应的键值关联成对相等时,它们才相等。
项顺序在 Erlang 规范 4.7 中定义,并引用如下
这些项主要根据其类型排序,顺序如下
numbers < atoms < refs < ports < PIDs < tuples < empty list < conses < binary
此处的规范不完整,实际的项顺序是
numbers < atoms < refs < funs < ports < pids < tuples < empty list < conses < binaries
Map 数据类型排在元组之后
numbers < atoms < refs < funs < ports < pids < tuples < maps < empty list < conses < binaries
----
然后,Map 首先按大小排序,然后按各自的键排序,最后按 Erlang 项顺序排序相关的值。
给定两个大小相同的 map M1
和 M2
,它们进行比较,以便 M1
中每个键(按 Erlang 项顺序排列)与 M2
的相应键进行比较。首先比较所有键,然后比较值,直到找到差异。如果键或值不同,则各自的项顺序(按 Erlang 项顺序排列)即为 map 的顺序。如果没有键值对不同,则认为 map 相等。
示例
> #{ 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
Map 以任意顺序打印键。
map 关联运算符和 set-value 运算符的优先级最低,排在匹配运算符和 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
=> :=
因此,map 表达式
#{ key => C = 1 + 2 }
将按以下顺序求值
#{ key => ( C = ( 1 + 2 ) ) }
模式匹配是一个非常强大的 Erlang 工具。Map 在其模式匹配中引入了一些新功能。
我们将使用匹配运算符进行举例。
Map 的模式匹配在表面上与记录相似。在 LHS 模式中请求的键将与在 RHS 关联 map 中找到的值绑定。
1> #{ a := V } = #{ a => 1 }.
#{ a => 1 }
2> 1 = V.
1
如果在 RHS map 中未找到 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 map 中键的关联值不匹配,则匹配会产生异常。
1> #{ a := 10 } = #{ a => 1 }.
** exception error: no match of right hand side value #{ a => 1 }
只有请求的键才会绑定到关联。匹配的 map 中存在的任何未请求的键都将被忽略。
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
请求的键的顺序在模式匹配 map 时没有意义。
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" }
下面的示例是一个构造的示例,用于说明 map 模式匹配的强大功能。
对匹配表达式求值,以便在求值之前绑定表达式中用作键的变量(如果可能)。
例如,键可以通过其他键值关联绑定。
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。
将变量绑定用作键需要绑定顺序是无环的。重新排序会扩展到匹配表达式中的所有项,因此
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 个标签可用于编码外部二进制分发的项。所有标签都在 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
指定了大小描述符后面的键和值的数量。
一个开放的问题,优化以下哪项?
内存大小应该是首要考虑的,因为我们需要通过网络发送这些数据。我们应该提高易于访问性,以便其他语言可以集成到此格式中。
这导致了扁平而简单的结构。因此,编码/解码会带来性能损失。
当已经有记录、字典、gb_trees、ets 和 proplists 时,为什么还需要映射?
映射被设想为一种易于使用、轻量级但功能强大的键值关联存储。
映射利用了 Erlang 的主要优势之一,模式匹配,来丰富用户体验并提供一个强大的工具来简化代码开发。在可用性方面,模式匹配使映射比字典、gb_trees 或 proplists 更具优势。
映射提供了将任意项作为键(不仅仅是原子),并将任意项作为值,存储在可匹配的数据类型中的可能性。
映射并不像 frames 提案那样声称要取代记录。相反,映射的目标是更大的使用领域,并希望成为记录的补充,并在合适的情况下取代它们。
映射最初并未被设想为记录的替代品,这是一个后来添加的期望要求。记录替代方法不一定限制任何语义,但可能会对实现和底层结构施加一些约束。
在适当的情况下,记录功能强大
然而,一些缺点是
在将映射与记录进行比较时,映射很容易弥补这些缺点,但是,在内置数据类型中,在运行时而不是在编译时确定值,很难复制积极的效果。
有人提出了使用 frames 或映射的更简单的 JSON 表示形式的论点。使用 frames 并因此使用原子来动态创建键将是一个严重的缺点。使用映射将允许使用字符串二进制文件来表示键,并且不会使全局原子池混乱。
更改 json-解码。Mochiweb 的 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),
使用映射,它可能看起来像这样
#{ "menu" := Menu } = mochijson:decode(Json),
#{ "popup" := PopUp } = Menu,
#{ "menuitem" := Items } = PopUp,
或者甚至
Decoded = mochijson:decode(Json),
#{ "menu" := #{ "popup" := #{ "menuitem" := Items } } } = Decoded,
使用映射和单值访问,它可以变得非常简单
Decoded = mochijson:decode(Json),
Items = Decoded#{ "menu" }#{ "popup" }#{ "menuitem "}.
我们有一些使用场景仍然存在争议。针对每个问题给出了一个建议的答案,该答案源于本提案中的讨论。
behavior
,而实际上想要更新键 behaviour
。此时,不是得到两个不同的键,而是会发生运行时异常。这些问题的答案对于我们应该如何设计和实现映射至关重要。我们还应该记住的是,我们永远无法完全摆脱记录,因此某些 frames 的论点可能是多余的。
如前所述,当前语法并非一成不变,但我们应该对其有何期望?
首先,它必须是明确的,这意味着语法必须产生单一、明确的行为,不能被人或机器误解。
1.1 在这里,我们也包括了类似语法应该具有类似行为的概念,或者至少不应该具有完全不同的行为。例如,记录 #record{ key = Value }
具有 O(1) 的性能和 2 + N 个字的内存开销。如果我们使用类似的语法,即 #{ key = Value }
,我们也应该期望类似的行为,包括语义和性能,对于映射的任何和所有大小都是如此。
语法必须尽可能短。只要不违反规则 1,它就不应该使用比描述特定行为所需更多的字符。
2.2 我们希望避免语言的冗长。冗长会把信息从我们的视野中推开,并使之模糊不清。这需要避免。
作者认为: =>
作为“设置或更新”中的分隔符, :=
作为“设置现有”和“匹配”语法中的分隔符。
在下面的示例中,我们使用 #{ Key => Value }
来描述映射的语义和用例,但这只是众多建议之一。
存在几种语法建议,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 匹配到映射模式,其中 Value 等于 key”。除非我们将赋值分隔符 :=
称为它不应该被称呼的东西,否则这并不容易理解。
然而,:=
也类似于 =:=
,后者表示“完全相等”,即匹配。这是一个有意义的含义,因为我们在处理数字时 ==
和 =:=
之间存在差异,因此 :=
可能是更正确的匹配语法分隔符。
如果不是因为它会重载函数子句的含义,那么分隔符 ->
将是一个合适的选择。
在处理二进制数据时,->
和 =>
都可能令人困惑。考虑 #{ <<"key">> -> <<"value">> }
和 #{ <<"key">> => <<"value">> }
,其中 =>
似乎比 ->
更令人困惑。
根据上述感知到的期望值列出分隔符:
#{ K => V }
- 没有歧义,没有重载,读作关联。#{ K := V }
- 没有歧义,没有重载,读作赋值或精确匹配。#{ K ~ V }
- 排版歧义,没有重载,没有明确的含义。#{ K -> V }
- 重载函数子句头和体分隔符,读作关联。#{ K = V }
- 重载匹配运算符,读作匹配或赋值。对现有键使用 :=
赋值似乎是一个不错的选择。对于设置或更新的选择是在 =>
和 ~
之间。
提出了在映射中更新值的两种不同方式的案例。
一种语法是,当我们只想更新已存在的键的值时,另一种是当我们想用任何键更新映射时。
M#{ K => V }
来声明新的键值对,或更新已存在的键。M#{ K := V }
来更新已存在的键。#{ K := V } = M
来匹配映射。示例 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)].
当操作属性列表时,文本影响,即字符数,非常大。
如果我们改用带有语法的某种映射,它会是什么样子?
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 个字符,而映射示例有 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 中才成立。在映射的情况下,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
它还允许轻松访问深层结构中的关联值。
单值访问的语法是此提案中最不完善(和考虑)的功能,当然可以借鉴一些意见。
此外,必须废除点语法。目前,它用于记录,但不会用于映射。点表示最后一个子句中表达式列表的结尾,或属性的结尾。
它不能用于区分浮点数或关联。
示例
1> M = #{ 1.1 => a, 1 => #{ 1 => b } }.
#{ 1 => #{ 1 => b }, 1.1 => a }.
2> #M.1.1.
a | b ?
使用映射编写的 Erlang 代码只能在 Erlang/OTP R17A 及更高版本的 Erlang/OTP 上解析、加载和执行,而不能在以前的版本上执行。
在 Erlang/OTP R17A 之前编写的 Erlang 代码将完全兼容,即可以使用这些映射更改进行解析、加载和执行。
分发将不向后兼容。
本文档已置于公共领域。