此 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
是任何 Erlang 表達式,其評估結果為 map()
或 maps:iterator()
資料類型。
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}
。
Map 迭代器允許在 maps:filter/2
、maps:fold/3
和 maps:map/2
中使用。由於推導式通常在與這些函數相同的上下文中使用,因此允許使用 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
可能會對效能造成問題,因為它是急切的,並且會在堆積區上產生垃圾。
現有的任何 Erlang 程式碼都不會受到影響,因為我們在此 EEP 中提出的語法擴充在目前的 Erlang 中不是有效的語法。使用 Map 推導式編寫的 Erlang 程式碼將無法被較舊的 Erlang 編譯器解析。某些使用中間 Erlang 形式(例如,AST)的工具必須更新(例如,解析轉換)。但是,我們預期核心 Erlang 形式不會受到影響 - 類似於列表和二進制推導式。
參考實作以 GitHub 上的提取請求形式提供
https://github.com/erlang/otp/pull/4856