檢視原始碼 Enumerable 協定 (Elixir v1.16.2)
由 Enum
和 Stream
模組使用的 Enumerable 協定。
當您在 Enum
模組中呼叫函數時,第一個參數通常是必須實作此協定的集合。例如,表達式 Enum.map([1, 2, 3], &(&1 * 2))
呼叫 Enumerable.reduce/3
來執行縮減運算,藉由對集合中的每個元素呼叫對應函數 &(&1 * 2)
並使用累積清單消耗元素,來建立對應清單。
在內部,Enum.map/2
的實作如下
def map(enumerable, fun) do
reducer = fn x, acc -> {:cont, [fun.(x) | acc]} end
Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
end
請注意,使用者提供的函數會封裝到 reducer/0
函數中。 reducer/0
函數必須在每個步驟後傳回標記元組,如 acc/0
類型中所述。最後,Enumerable.reduce/3
傳回 result/0
。
此協定使用標記元組在縮減函數和實作協定的資料類型之間交換資訊。這允許有效率地列舉資源(例如檔案),同時也保證資源會在列舉結束時關閉。此協定也允許暫停列舉,這在需要在許多列舉之間交錯時很有用(例如在 zip/1
和 zip/2
函數中)。
此協定需要實作四個函數,reduce/3
、count/1
、member?/2
和 slice/1
。此協定的核心是 reduce/3
函數。所有其他函數都是針對資料結構的最佳化路徑,這些資料結構可以用比線性時間更好的時間來實作特定屬性。
摘要
類型
每個步驟的累加器值。
一個部分應用之縮減函式。
縮減函式。
縮減運算的結果。
一個切片函式,接收初始位置、切片中的元素數量和步驟。
所有實作此協定的類型。
類型 element
元素的可列舉。
接收一個可列舉並傳回一個清單。
類型
每個步驟的累加器值。
它必須是一個標記元組,具有下列其中一個「標記」
:cont
- 列舉應該繼續:halt
- 列舉應該立即停止:suspend
- 列舉應該立即暫停
根據累加器值,Enumerable.reduce/3
傳回的結果將會改變。請查看 result/0
類型文件以取得更多資訊。
如果 reducer/0
函式傳回 :suspend
累加器,它必須由呼叫者明確處理,且絕不洩漏。
一個部分應用之縮減函式。
當列舉暫停時,延續是作為結果傳回的閉包。當呼叫時,它預期一個新的累加器,並傳回結果。
只要縮減函式以尾遞迴方式定義,延續就可以輕易實作。如果函式是尾遞迴,所有狀態都會作為引數傳遞,因此延續就是部分應用的縮減函式。
縮減函式。
應該呼叫 enumerable
元素和累加器內容。
傳回下一個列舉步驟的累加器。
@type result() :: {:done, term()} | {:halted, term()} | {:suspended, term(), continuation()}
縮減運算的結果。
當列舉完成並到達其結尾時,它可能會完成,或者當列舉因標記累加器而停止或暫停時,它可能會停止/暫停。
如果給定標記 :halt
累加器,則必須傳回具有累加器的 :halted
元組。像 Enum.take_while/2
這樣的函式在內部使用 :halt
,可用於測試終止列舉。
如果給定標記 :suspend
累加器,呼叫者必須傳回具有累加器和延續的 :suspended
元組。呼叫者接著負責管理延續,且呼叫者必須始終呼叫延續,最終終止或繼續直到結束。 Enum.zip/2
使用暫停,因此可用於測試您的實作是否正確處理暫停。您也可以將 Stream.zip/2
與 Enum.take_while/2
搭配使用,以測試 :suspend
與 :halt
的組合。
@type slicing_fun() :: (start :: non_neg_integer(), length :: pos_integer(), step :: pos_integer() -> [term()])
一個切片函式,接收初始位置、切片中的元素數量和步驟。
start
位置是數字 >= 0
,且保證存在於 enumerable
中。長度是數字 >= 1
,方式為 start + length * step <= count
,其中 count
是列舉中元素的最大數量。
函式應傳回非空清單,其中元素的數量等於 length
。
@type t() :: term()
所有實作此協定的類型。
@type t(_element) :: t()
類型 element
元素的可列舉。
此類型等同於 t/0
,但對於文件特別有用。
例如,假設您定義一個函式,它預期一個整數列舉,並傳回一個字串列舉
@spec integers_to_strings(Enumerable.t(integer())) :: Enumerable.t(String.t())
def integers_to_strings(integers) do
Stream.map(integers, &Integer.to_string/1)
end
接收一個可列舉並傳回一個清單。
函式
@spec count(t()) :: {:ok, non_neg_integer()} | {:error, module()}
擷取 enumerable
中的元素數量。
如果您能以比完全遍歷更快的速度計算 enumerable
中的元素數量,則應傳回 {:ok, count}
。
否則,它應傳回 {:error, __MODULE__}
,以及建立在 reduce/3
上的預設演算法,該演算法以線性時間執行。
檢查 element
是否存在於 enumerable
中。
如果您可以在不遍歷整個 enumerable
的情況下,使用 ===/2
檢查給定元素在 enumerable
中的成員資格,則應傳回 {:ok, boolean}
。
否則,它應傳回 {:error, __MODULE__}
,以及建立在 reduce/3
上的預設演算法,該演算法以線性時間執行。
將 enumerable
縮減成一個元素。
Enum
中的大部分操作都是使用 reduce 函式實作的。此函式應將給定的 reducer/0
函式套用至 enumerable
中的每個元素,並按照傳回的累加器預期的方式進行。
請參閱類型 result/0
和 acc/0
的文件以取得更多資訊。
範例
舉例來說,以下是清單的 reduce
實作
def reduce(_list, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)}
def reduce([], {:cont, acc}, _fun), do: {:done, acc}
def reduce([head | tail], {:cont, acc}, fun), do: reduce(tail, fun.(head, acc), fun)
@spec slice(t()) :: {:ok, size :: non_neg_integer(), slicing_fun() | to_list_fun()} | {:error, module()}
傳回一個連續切片資料結構的函式。
它應傳回下列其中一項
{:ok, size, slicing_fun}
- 如果enumerable
有已知的界線,且可以在不遍歷所有先前元素的情況下存取enumerable
中的位置。slicing_fun
將會收到一個start
位置、要擷取的元素amount
以及一個step
。{:ok, size, to_list_fun}
- 如果enumerable
有已知的界線,且可以在透過to_list_fun
先將其轉換為清單後,存取enumerable
中的位置。{:error, __MODULE__}
- 無法有效率地切片可列舉物件,且將會使用建立在reduce/3
之上的預設演算法,該演算法會以線性時間執行。
與 count/1
的差異
此函式傳回的 size
值用於邊界檢查,因此非常重要的是此函式僅在擷取 enumerable
的 size
便宜、快速且需要常數時間時,才傳回 :ok
。否則,最簡單的操作(例如 Enum.at(enumerable, 0)
)將會變得太昂貴。
另一方面,只要您可以在不遍歷集合的情況下計算集合中的元素數量,就應該實作此協定中的 count/1
函式。