此 EEP 提议了 map 推导式和生成器的语法和语义,类似于列表和二进制推导式和生成器,但操作的是 map 和 map 迭代器。
本文档置于公共领域或 CC0-1.0-Universal 许可之下,以较宽松者为准。
Map 和初始的 map 推导式语法和语义在 EEP-43 中提出。列表和二进制推导式在 EEP-12 中事后指定。此 EEP 基于这两个 EEP。
通常被称为“推导式”的语法结构,实际上由 3 个半独立的部分组成
false
时,当前元素将被跳过,不会传递给“推导式”;当结果为 true
时,该元素将传递给“推导式”。因此,在本文档中,我们将保持这种分离,但有时会将推导式、过滤器和生成器的整个组合称为“推导式”,因为生成器语法不在推导式上下文之外使用。
map 推导式部分的建议语法是
'#{' KeyExpr '=>' ValueExpr '||' GeneratorsFilters '}'
其中 GeneratorsFilters
是一个或多个列表/map/二进制生成器和/或过滤器,用逗号分隔。
map 生成器部分的建议语法是
KeyPattern ':=' ValuePattern '<-' MapOrIteratorExpr
map 推导式和 map 生成器的示例组合
#{Key => Value || Key := Value <- MapOrIterator}
Map 推导式可以与列表或二进制生成器组合,反之亦然
[{Key, Value} || Key := Value <- MapOrIterator]
#{K => K || K <- List}
#{K => V || <<K, V>> <= Binary}
MapOrIterator
是任何求值为 map()
或 maps:iterator()
数据类型的 Erlang 表达式。
Map 推导式在语义上应等效于在生成 2 元组列表的列表推导式上调用 maps:from_list/1
#{K => V || ...}
等效于
maps:from_list([{K, V} || ...])
当输入为 map 数据类型时,map 生成器在语义上应等效于在 maps:to_list/1
的结果上运行的列表生成器
#{ ... || K := V <- Map}
等效于
#{ ... || {K, V} <- maps:to_list(Map)}
对于 maps:iterator()
作为输入 - 它不能轻易地表达为等效的列表生成器。但其思想是,它等效于通过重复调用 maps:next/1
来消耗迭代器,并在每次迭代循环中分别在键和值模式上匹配 Key
和 Value
。
如果 MapOrIterator
表达式的求值结果不是 map 或 map 迭代器,则 map 生成器应引发 error({bad_generator, ExprResult})
。
允许键模式为匿名变量 _
/ 包含未绑定的变量(这在其他 map 模式匹配上下文中是不允许的)。
变量绑定和隐藏规则应与列表和二进制推导式相同(这遵循上述等效规则)。
我们决定采用 EEP-43 中提出的语法。
#{ .. }
被选为 map 推导式的外部标记,因为在构造新 map 时使用相同的标记 - 类似于列表和二进制推导式。
K := V <-
对于解析器没有歧义,因此与二进制生成器不同,不需要新的箭头。选择 <-
而不是 <=
,因为后者当放置在相对靠近 =>
和 :=
标记时,视觉上不太容易区分
#{K => V || K := V <= Map}.
% vs
#{K => V || K := V <- Map}.
KeyPattern := ValuePattern
与 map 模式匹配中使用的语法相同。但是,在 map 生成器中,KeyPattern
也允许是匿名或未绑定的变量。
KeyExpr => ValueExpr
与新 map 构造中使用的语法相同。
由于 map 没有指定任何顺序,因此不应保证生成器产生键值对的顺序(与从 maps:to_list/1
生成的列表的顺序不保证相同)。
与 maps:from_list/1
相同:使用最新的(最后生成的)值,并忽略先前的值。当 map 推导式与 map 生成器结合使用时,它可能不太可预测,因为 map 生成器顺序未定义。例如
#{ V => K || K := V <- #{a => x, b => x} }
可能会生成 #{x => a}
和 #{x => b}
。
但是当 map 推导式与列表或二进制生成器一起使用时,它变得很重要。例如
#{ V => K || {K, V} <- [{a, x}, {b, x}] }
将始终生成 #{x => b}
。
在 maps:filter/2
、maps:fold/3
和 maps:map/2
中允许使用 map 迭代器。由于推导式经常在与这些函数相同的上下文中使用,因此允许使用 map 迭代器是有意义的,这样推导式可以用作上述函数的替代品。
是否允许使用 map 推导式进行 map 更新?也就是说,这应该是有效的语法吗
MapToUpdate#{ K => V*2 || K := V <- MapToUpdateWith}
或者这个
MapToUpdate#{ K := V*2 || K := V <- MapToUpdateWith}
关于它可能有多有用,目前还没有最终决定。
使用 maps:merge/2
函数可以实现或多或少相同的结果(优化空间较小)。
反对它的一个(可能很小的)论点是,“map 更新”语法是程序员忘记在两个 map 之间添加逗号时出现非常令人困惑的错误的一个常见原因
> [#{a => b}
#{a => d}].
[#{a => d}]
这里意外地创建了“map 更新”语法,而不是预期的“两个 map 的列表”。
存在类似问题可能发生在“通过推导式更新”中的一些风险
my_fun(Map) ->
Options = #{a => b}
#{ K => V || K := V <- smth(Map, Options) }.
但是概率较低,因为错误通常发生在文字定义中,但程序员很少在文字定义中使用推导式。
有了列表和二进制的推导式和生成器,似乎 map 也应该有一个生成器是合乎逻辑的。例如,Python 语言具有列表和字典的推导式版本(但推导式中的生成器也适用于任何可迭代对象)。另一个因素是在实际代码中,经常看到 maps:from_list/1
、maps:to_list/1
与列表推导式和列表高阶函数的组合,在 maps
模块中的高阶函数不够灵活的情况下(例如,我们希望将 map 转换为其他结构,而不是另一个 map,或者同时修改键和值)。但是使用 maps:from/to_list
可能会对性能造成问题,因为它很急切并在堆上创建垃圾。
由于我们在本 EEP 中提出的语法扩展目前在 Erlang 中不是有效的语法,因此现有的 Erlang 代码都不会受到影响。使用 map 推导式编写的 Erlang 代码将无法被旧的 Erlang 编译器解析。某些使用中间 Erlang 形式(例如,AST)的工具必须更新(例如,解析转换)。但是,我们希望核心 Erlang 形式不受影响 - 类似于列表和二进制推导式。
参考实现以 GitHub 上的拉取请求的形式提供
https://github.com/erlang/otp/pull/4856