作者
Sergey Prokhorov <seriy(点)pr(在)gmail(点)com>
状态
最终/26.0 已在 OTP 版本 26 中实现
类型
标准跟踪
创建
2021-09-14
Erlang 版本
OTP-26.0
发布历史
2021-05-20, https://github.com/erlang/otp/pull/4856

EEP 58: Map 推导式 #

摘要 #

此 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 来消耗迭代器,并在每次迭代循环中分别在键和值模式上匹配 KeyValue

如果 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 生成器产生键值对的顺序 #

由于 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}

是否允许将 map 迭代器作为 map 生成器的输入 #

maps:filter/2maps:fold/3maps:map/2 中允许使用 map 迭代器。由于推导式经常在与这些函数相同的上下文中使用,因此允许使用 map 迭代器是有意义的,这样推导式可以用作上述函数的替代品。

通过 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/1maps: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