作者
Isabell Huang <isabell(at)erlang(dot)org>
状态
最终/28.0 在 OTP 版本 28 中实现
类型
标准跟踪
创建时间
2024-09-21
Erlang 版本
OTP-28.0
取代
19

EEP 73:Zip 生成器 #

摘要 #

此 EEP 提议在 Erlang 中为推导式添加 && 语法的 zip 生成器。zip 生成器(推导式多重生成器)的想法和语法最早由 EEP-19 提出。即使此 EEP 提出的 zip 生成器的语法和用法与 EEP-19 大致相同,但自从 EEP-19 被接受以来,Erlang 的推导式语言已经发生了许多变化。通过一个与所有现有推导式兼容的实现,此 EEP 定义了 zip 生成器的行为,并对编译器的部分进行了更详细的说明。

理由 #

列表推导式是一种从现有列表创建一个新列表的方法。列表以依赖(嵌套)方式遍历。在下面的示例中,当两个输入列表的长度都为 2 时,结果列表的长度为 4。

[{X,Y} || X <- [1,2], Y <- [3,4]] = [{1,3}, {1,4}, {2,3}, {2,4}].

相反,并行列表推导式(也称为 zip 推导式)并行评估限定符(列表的泛化)。限定符首先“压缩”在一起,然后进行评估。许多函数式语言(HaskellRacket 等)和非函数式语言(Python 等)都支持这种变体。假设上面示例中的两个列表被评估为 zip 推导式,结果将是 [{1,3}, {2,4}]

Zip 推导式允许用户方便地一次迭代多个列表。如果没有它,在 Erlang 中完成相同任务的标准方法是使用 lists:zip 将两个列表压缩为二元组,或使用 lists:zip3 将三个列表压缩为三元组。list 模块不提供压缩三个以上列表的函数。编译时,像 lists:zip 这样的函数总是会创建中间数据结构。编译器不会执行森林砍伐来消除不需要的元组。

Zip 生成器是 zip 推导式的泛化。每组压缩的生成器都被视为一个生成器。任何生成器都可以是 zip 生成器(包含至少两个压缩在一起的生成器),也可以是非 zip 生成器(所有现有生成器都是非 zip 生成器),而不是将推导式约束为压缩或非压缩。因此,zip 生成器可以与所有现有生成器和过滤器自由混合。然后,zip 推导式成为仅使用 zip 生成器的推导式的特例。

在 OTP 代码库中,在推导式中有很多使用 lists:zip 的情况。所有这些都可以通过使用 && 语法的 zip 生成器来简化。例如,parsetools 中的 yecc.erl 包含以下推导式(为了可读性,外部函数调用和不相关的字段已被删除)

PartDataL = [#part_data{name = Nm, eq_state = Eqs, actions = P, states = S}
    || {{Nm,P}, {Nm,S}, {Nm,EqS}} <- 
                    lists:zip3(PartNameL, PartInStates, PartStates)].

当使用 zip 生成器时,推导式被重写为

PartDataL = [#part_data{name = Nm, eq_state = Eqs, actions = P, states = S}
    || {Nm,P} <- PartNameL && {Nm,S} <- PartInStates && {Nm,EqS} <- PartStates].

通过使用 zip 生成器,编译器避免了构建中间元组列表的需要。zip 生成器中的变量绑定和模式匹配按预期工作,因为 Nm 应该绑定到 {Nm,P}{Nm,S} 中的相同值。如果绑定失败,则会跳过来自 3 个生成器中的每个生成器的 1 个元素。(如果使用严格生成器,则推导式将因异常而失败,如 错误行为 中指定的那样。)

总而言之,zip 生成器消除了用户在推导式中调用 zip 函数的需要,并允许一次压缩任意数量的列表。它可以在列表、二进制和映射推导式中使用,并与所有现有生成器和过滤器自由混合。在内部,编译器不会创建任何中间数据结构,因此也消除了森林砍伐的需要。

规范 #

目前,Erlang 支持三种推导式:列表推导式、二进制推导式和映射推导式。它们的名称是指推导式的结果。列表推导式生成列表;二进制推导式生成二进制数据等。

[Expression || Qualifier(s)]                     %% List Comprehension
<<Expression || Qualifier(s)>>                   %% Binary Comprehension
#{Expression || Qualifier(s)}                    %% Map Comprehension

限定符可以有以下几种:过滤器、列表生成器、位串生成器和映射生成器。除了过滤器之外,其他三种限定符都是生成器。它们的名称是指 <-<= 右侧的类型。生成器具有以下形式

Pattern <- List                                  %% List Generator
Pattern <= Bitstring                             %% Bitstring Generator
Pattern_1 := Pattern_2 <- Map                    %% Map Generator

所有限定符和过滤器都可以在所有 3 种推导式中自由使用和混合。以下示例显示了一个带有列表生成器和位串生成器的列表推导式。

[{X,Y} || X <- [1,2,3], <<Y>> <= <<4,5,6>>].

此 EEP 提议添加 zip 生成器。zip 生成器是由 && 连接的两个或多个生成器。zip 生成器被构造为连接上述 3 种生成器中的任意数量的生成器。zip 生成器可以以相同的方式在列表、二进制或映射推导式中使用。

例如,如果上面示例中的两个生成器组合在一起作为 zip 生成器,则推导式将如下所示

[{X,Y} || X <- [1,2,3] && <<Y>> <= <<4,5,6>>].

对于形式为 G1 && ... && Gn 的每个 zip 生成器,它被评估为具有与 zip/n 相同的结果,其中

zip([H1|T1], ..., [Hn|Tn]) ->
    [{H1,...,Hn} | zip(T1, ..., Tn)];
zip([], ..., []) ->
    [].

因此,上面的推导式评估为 [{1,4}, {2,5}, {3,6}],这与使用 lists:zip/2 相同。

当推导式包含其他非 zip 生成器和/或过滤器时,也可以使用 zip 生成器。&& 符号的优先级高于 ,

以下示例评估为 [{b,4}, {c,6}]。元素 {a,2} 从结果列表中过滤掉。

[{X, Y} || X <- [a, b, c] && <<Y>> <= <<2, 4, 6>>, Y =/= 2].

始终会遵循 zip 生成器中单个生成器的跳过行为。当 zip 生成器中存在严格生成器时,在每一轮评估期间,即使另一个宽松生成器已经导致结果被跳过,也会评估所有严格生成器。

例如,以下推导式具有一个包含严格生成器和宽松生成器的 zip 生成器。

[{X, Y} || {a, X} <:- L1 && {b, Y} <- L2]

如果 {a, X} 的模式匹配失败,例如,当 L1 = [{a, 1}, {bad, 2}, {a, 3}] 时,它将 badarg。如果 {b, Y} 的模式匹配失败,例如,当 L2 = [{b, 1}, {bad, 2}, {b, 3}] 时,它将简单地从 L1L2 中跳过 1 项。无论生成器顺序以及其他模式匹配是否成功,都会在每一轮尝试匹配 {a, X}

与使用辅助函数相比,使用 zip 生成器有一个优点:当 zip 生成器转换为核心 Erlang 时,Erlang 编译器不会生成任何元组。生成的代码反映了程序员的意图,即每次从每个列表中收集一个元素,而不创建元组列表。

错误行为 #

人们会期望当发生错误时,zip 生成器的行为与 lists:zip/2lists:zip3/3 以及当压缩 3 个以上列表时上面的 zip/n 函数相同。zip 生成器的设计和实现旨在在已编译的代码和在 Erlang shell 中评估的推导式中都实现这一点。

不同长度的生成器 #

如果给定的列表长度不同,lists:zip/2lists:zip3/3 将会失败,而 zip/n 也会崩溃。因此,当 zip 生成器发现给定的生成器长度不同时,它会引发 bad generators 错误。

当 zip 生成器因包含的生成器长度不同而崩溃时,内部错误消息是一个元组,其中第一个元素是原子 bad_generators,第二个元素是一个包含来自所有生成器的剩余数据的元组。面向用户的错误消息是 bad generators:,后跟包含来自所有生成器的剩余数据的元组。

例如,此推导式将在运行时崩溃。

[{X,Y} || X <- [1,2,3] && Y <- [1,2,3,4]].

结果错误元组是 {bad_generators,{[],[4]}}。这是因为当推导式崩溃时,zip 生成器中的第一个列表只剩下空列表 [],而 zip 生成器中的第二个列表剩下 [4]

在编译器方面,很难在错误消息中返回原始的 zip 生成器,或者指出哪个生成器与其他生成器相比长度不同。提出的错误消息旨在提供最有用的信息,而不会给编译器或运行时带来额外的负担。

Zip 生成器中失败的严格生成器 #

当 zip 生成器因其中包含的至少一个严格生成器失败而崩溃时,生成的错误元组的格式与生成器长度不同时相同。它的第一个元素是原子 bad_generators,第二个元素是一个包含来自所有生成器的剩余数据的元组。

例如,此推导式将在运行时崩溃,因为 bad 无法匹配模式 {ok,A}

[A + B || {ok,A} <:- [bad, {ok,1}] && B <- [2,3]].

结果错误元组是 {bad_generators,{[bad, {ok,1}],[2,3]}}。尽管严格生成器单独失败时会产生异常 badmatch,如 EEP-70 中指定的那样,但在 zip 生成器中使用相同的异常是不可行的,因为难以区分 badmatchbad_generators 错误。

在以下示例中,推导式将在运行时崩溃,可能是由于严格生成器失败,也可能是由于两个生成器长度不同。

[A + B || {ok,A} <:- [bad] && B <- []].

发出的错误消息是 {bad_generators,{[bad],[]}}。我们不区分这两个错误,而是始终输出生成器中的所有剩余数据。用户可以检查剩余数据,并看到第一个生成器匹配失败,第二个生成器为空。

Zip 生成器中的非生成器 #

由于压缩 (zipping) 的概念仅对生成器 (generators) 有意义,因此 zip 生成器不能包含过滤器 (filters) 或任何非生成器的表达式。如果能在编译时捕获此类错误,Erlang 代码检查器 (linter) 会捕获此错误。

例如,以下推导式中的 zip 生成器包含一个过滤器。

zip() -> [{X,Y} || X <- [1,2,3] && Y <- [1,2,3] && X > 0].

当编译该函数时,代码检查器会指出 zip 生成器中只允许生成器,并会同时指出非生成器的位置。

t.erl:6:55: only generators are allowed in a zip generator.
%    6|  zip() -> [{X,Y} || X <- [1,2,3] && Y <- [1,2,3] && X > 0].
%     |                                                       ^

向后兼容性 #

&& 运算符在 Erlang 中未使用。此添加不会影响任何现有代码。

参考实现 #

PR-8926 包含 zip 生成器的实现。

版权 #

本文档置于公有领域或 CC0-1.0-Universal 许可之下,以更宽松的许可为准。