此 EEP 提議在 Erlang 的 comprehension 中加入語法為 &&
的 zip 產生器。zip 產生器(comprehension 多重產生器)的想法和語法最初由 EEP-19 提出。即使此 EEP 提出的 zip 產生器語法和用法與 EEP-19 大致相同,自 EEP-19 通過以來,Erlang 的 comprehension 語言已經歷了許多變化。通過與所有現有 comprehension 相容的實作,此 EEP 更明確地定義了 zip 產生器的行為(從編譯器的角度來看)。
List comprehension 是一種從現有列表建立新列表的方法。列表以相依(巢狀)方式遍歷。在以下範例中,當兩個輸入列表的長度都為 2 時,產生的列表長度為 4。
[{X,Y} || X <- [1,2], Y <- [3,4]] = [{1,3}, {1,4}, {2,3}, {2,4}].
相反地,並行列表 comprehension(也稱為 zip comprehension)會並行評估限定詞(列表的概括)。限定詞首先「壓縮」在一起,然後進行評估。許多函數式語言(Haskell、Racket 等)和非函數式語言(Python 等)都支援這種變體。假設上面範例中的兩個列表被評估為 zip comprehension,結果將為 [{1,3}, {2,4}]
。
Zip comprehension 允許使用者方便地同時迭代多個列表。如果沒有它,在 Erlang 中完成相同任務的標準方法是使用 lists:zip
將兩個列表壓縮為二元組,或使用 lists:zip3
將三個列表壓縮為三元組。列表模組不提供壓縮超過三個列表的函數。像 lists:zip
這樣的函數在編譯時總是會建立中間資料結構。編譯器不會執行去森林化來消除不需要的元組。
Zip 產生器是 zip comprehension 的概括。每一組壓縮的產生器都被視為一個產生器。任何產生器都可以是 zip 產生器(包含至少兩個壓縮在一起的產生器),或者是非 zip 產生器(所有現有產生器都是非 zip 產生器),而不是將 comprehension 約束為壓縮或非壓縮。因此,zip 產生器可以與所有現有的產生器和過濾器自由混合。然後,zip comprehension 成為只使用 zip 產生器的 comprehension 的特殊情況。
在 OTP 程式碼庫中,comprehension 中有很多使用 lists:zip
的地方。所有這些都可以通過使用 &&
語法的 zip 產生器來簡化。例如,parsetools
中的 yecc.erl
包含以下 comprehension(為了易讀性,已刪除外部函數呼叫和不相關的欄位)
PartDataL = [#part_data{name = Nm, eq_state = Eqs, actions = P, states = S}
|| {{Nm,P}, {Nm,S}, {Nm,EqS}} <-
lists:zip3(PartNameL, PartInStates, PartStates)].
當使用 zip 產生器時,comprehension 會被重寫為
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 個元素。(如果使用嚴格產生器,則 comprehension 會因異常而失敗,如錯誤行為中所指定。)
總而言之,zip 產生器消除了使用者在 comprehension 中呼叫 zip 函數的需求,並允許同時壓縮任意數量的列表。它可以應用於列表、二進制和 map comprehension,並與所有現有的產生器和過濾器自由混合。在內部,編譯器不會建立任何中間資料結構,因此也消除了去森林化的需求。
目前,Erlang 支援三種類型的 comprehension:列表 comprehension、二進制 comprehension 和 map comprehension。它們的名稱是指 comprehension 的結果。列表 comprehension 產生一個列表;二進制 comprehension 產生一個二進制,等等。
[Expression || Qualifier(s)] %% List Comprehension
<<Expression || Qualifier(s)>> %% Binary Comprehension
#{Expression || Qualifier(s)} %% Map Comprehension
限定詞可以有以下種類:過濾器、列表產生器、位元字串產生器和 map 產生器。除了過濾器之外,其他三種限定詞都是產生器。它們的名稱是指 <-
或 <=
右側的類型。產生器具有以下形式
Pattern <- List %% List Generator
Pattern <= Bitstring %% Bitstring Generator
Pattern_1 := Pattern_2 <- Map %% Map Generator
所有限定詞和過濾器都可以在所有 3 種 comprehension 中自由使用和混合。以下範例顯示了具有列表產生器和位元字串產生器的列表 comprehension。
[{X,Y} || X <- [1,2,3], <<Y>> <= <<4,5,6>>].
此 EEP 提議加入 zip 產生器。zip 產生器是由 &&
連接的兩個或多個產生器。zip 產生器的建構目的是為了連接上述 3 種類型的任意數量的產生器。zip 產生器可以以相同的方式在列表、二進制或 map comprehension 中使用。
例如,如果將上述範例中的兩個產生器合併為 zip 產生器,則 comprehension 看起來會像這樣
[{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([], ..., []) ->
[].
因此,上面的 comprehension 評估結果為 [{1,4}, {2,5}, {3,6}]
,這與使用 lists:zip/2
的結果相同。
當 comprehension 包含其他非 zip 產生器和/或過濾器時,也可以使用 zip 產生器。&&
符號的優先順序高於 ,
。
以下範例的評估結果為 [{b,4}, {c,6}]
。元素 {a,2}
從產生的列表中過濾掉。
[{X, Y} || X <- [a, b, c] && <<Y>> <= <<2, 4, 6>>, Y =/= 2].
始終會遵循 zip 產生器中個別產生器的跳過行為。當 zip 產生器中存在嚴格產生器時,在每一輪評估期間,即使另一個寬鬆產生器已經導致結果被跳過,也將評估所有嚴格產生器。
例如,以下 comprehension 具有包含嚴格產生器和寬鬆產生器的 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}]
時,它只會從 L1
跳過 1 個項目,並從 L2
跳過 1 個項目。無論產生器的順序如何以及其他模式匹配是否成功,都會在每一輪嘗試匹配 {a, X}
。
與使用輔助函數相比,使用 zip 產生器有一個優點:當 zip 產生器轉換為核心 Erlang 時,Erlang 編譯器不會產生任何元組。產生的程式碼反映了程式設計師的意圖,即一次從每個列表中收集一個元素,而不建立元組列表。
人們會預期,當發生錯誤時,zip 產生器的行為與 lists:zip/2
、lists:zip3/3
相同,並且當壓縮超過 3 個列表時,也與上面的 zip/n
函數相同。zip 產生器的設計和實作旨在使編譯程式碼和 Erlang shell 中評估的 comprehension 都能實現這一點。
如果給定的列表長度不相同,lists:zip/2
和 lists:zip3/3
將會失敗,而 zip/n
也會當機。因此,當 zip 產生器發現給定的產生器長度不同時,會引發 bad generators
錯誤。
當 zip 產生器因為包含的產生器長度不同而當機時,內部錯誤訊息是一個元組,其中第一個元素是原子 bad_generators
,第二個元素是一個元組,其中包含來自所有產生器的剩餘資料。使用者看到的錯誤訊息是 bad generators:
,後接包含來自所有產生器的剩餘資料的元組。
例如,此 comprehension 將在執行時當機。
[{X,Y} || X <- [1,2,3] && Y <- [1,2,3,4]].
產生的錯誤元組是 {bad_generators,{[],[4]}}
。這是因為當 comprehension 當機時,zip 產生器中的第一個列表只剩下空列表 []
,而 zip 產生器中的第二個列表剩下 [4]
。
在編譯器方面,很難在錯誤訊息中返回原始的 zip 產生器,或指出哪個產生器的長度與其他產生器不同。提出的錯誤訊息旨在提供最有用的資訊,而不會給編譯器或執行時帶來額外的負擔。
當 zip 產生器因為其中包含的至少一個嚴格產生器失敗而當機時,產生的錯誤元組格式與產生器長度不同時相同。其第一個元素是原子 bad_generators
,第二個元素是一個元組,其中包含來自所有產生器的剩餘資料。
例如,此 comprehension 將在執行時當機,因為 bad
無法與模式 {ok,A}
匹配。
[A + B || {ok,A} <:- [bad, {ok,1}] && B <- [2,3]].
產生的錯誤元組是 {bad_generators,{[bad, {ok,1}],[2,3]}}
。儘管嚴格產生器單獨會因異常 badmatch
而失敗,如 EEP-70 中指定的那樣,但在 zip 產生器中使用相同的異常是不合理的,因為很難區分 badmatch
和 bad_generators
錯誤。
在以下範例中,comprehension 將在執行時當機,可能是因為嚴格產生器失敗,也可能是因為兩個產生器的長度不同。
[A + B || {ok,A} <:- [bad] && B <- []].
發出的錯誤訊息是 {bad_generators,{[bad],[]}}
。我們不區分這兩個錯誤,而是始終輸出產生器中的所有剩餘資料。使用者可以檢查剩餘資料,並看到第一個產生器匹配失敗,而第二個產生器為空。
由於壓縮的概念僅對產生器有意義,因此 zip 產生器不能包含過濾器或任何不是產生器的表達式。只要有可能在編譯時捕獲此類錯誤,Erlang linter 就會捕獲此錯誤。
例如,以下 comprehension 中的 zip 產生器包含一個過濾器。
zip() -> [{X,Y} || X <- [1,2,3] && Y <- [1,2,3] && X > 0].
編譯函數時,linter 會指出 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-通用許可證之下,以較寬鬆者為準。