作者
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 是任何 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 來使用迭代器,並在每個迭代迴圈中根據鍵和值模式匹配 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 生成器的輸入 #

Map 迭代器允許在 maps:filter/2maps:fold/3maps:map/2 中使用。由於推導式通常在與這些函數相同的上下文中使用,因此允許使用 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 可能會對效能造成問題,因為它是急切的,並且會在堆積區上產生垃圾。

向後相容性 #

現有的任何 Erlang 程式碼都不會受到影響,因為我們在此 EEP 中提出的語法擴充在目前的 Erlang 中不是有效的語法。使用 Map 推導式編寫的 Erlang 程式碼將無法被較舊的 Erlang 編譯器解析。某些使用中間 Erlang 形式(例如,AST)的工具必須更新(例如,解析轉換)。但是,我們預期核心 Erlang 形式不會受到影響 - 類似於列表和二進制推導式。

參考實作 #

參考實作以 GitHub 上的提取請求形式提供

https://github.com/erlang/otp/pull/4856