檢視原始碼 列舉和串流
雖然 Elixir 允許我們撰寫遞迴程式碼,但我們對集合執行的多數運算都是透過 Enum
和 Stream
模組完成的。讓我們來學習如何執行。
列舉
Elixir 提供列舉的概念和 Enum
模組來處理它們。我們已經學習了兩個列舉:清單和對應。
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]
Enum
模組提供許多函式,用於轉換、排序、群組、篩選和擷取列舉中的項目。這是開發人員在 Elixir 程式碼中經常使用的模組之一。若要深入了解 Enum
模組中的所有函式,請參閱 Enum
秘笈。
Elixir 也提供範圍(請參閱 Range
),它們也是列舉。
iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6
Enum
模組中的函式僅限於列舉資料結構中的值,如同其名稱所示。對於特定運算,例如插入和更新特定元素,您可能需要使用特定於資料類型的模組。例如,如果您想在清單中的特定位置插入元素,您應該使用 List.insert_at/3
函式,因為將值插入範圍中幾乎沒有意義。
我們說 Enum
模組中的函式是多型的,因為它們可以處理不同的資料類型。特別是, Enum
模組中的函式可以使用任何實作 Enumerable
協定的資料類型。我們將在後面的章節中討論協定,現在我們將繼續探討一種稱為串流的特定列舉。
急切與延遲
Enum
模組中的所有函式都是急切的。許多函式預期列舉並傳回清單
iex> odd? = fn x -> rem(x, 2) != 0 end
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]
這表示當使用 Enum
執行多個運算時,每個運算都會產生一個中間清單,直到我們得到結果
iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum()
7500000000
上面的範例有一個運算管線。我們從一個範圍開始,然後將範圍中的每個元素乘以 3。這個第一個運算現在會建立並傳回一個包含 100_000
項目的清單。然後我們保留清單中的所有奇數元素,產生一個新的清單,現在有 50_000
個項目,然後我們對所有項目求和。
管道運算子
上面程式片段中使用的 |>
符號是管道運算子:它會取得其左側表達式的輸出,並將其作為其右側函式呼叫的第一個引數傳遞。它的目的是強調由一系列函式轉換的資料。若要了解它如何讓程式碼更簡潔,請查看上面沒有使用 |>
運算子的範例重寫版本
iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000
透過閱讀其文件,進一步了解管道運算子。
串流
作為 Enum
的替代方案,Elixir 提供支援延遲運算的 Stream
模組
iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum()
7500000000
串流是延遲、可組合的列舉。
在上面的範例中,1..100_000 |> Stream.map(&(&1 * 3))
傳回一個資料類型,一個實際的串流,它表示範圍 1..100_000
上的 map
計算
iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>
此外,它們是可組合的,因為我們可以串接許多串流運算
iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>
串流不會產生中間清單,而是建立一系列計算,這些計算僅在我們將底層串流傳遞給 Enum
模組時才會呼叫。當處理大型(可能無限)集合時,串流很有用。
Stream
模組中的許多函式接受任何列舉作為引數,並傳回一個串流作為結果。它也提供建立串流的函式。例如,Stream.cycle/1
可用於建立一個串流,該串流會無限循環一個給定的列舉。小心不要對此類串流呼叫像 Enum.map/2
這樣的函式,因為它們會永遠循環
iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.unfold/2>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
另一方面,Stream.unfold/2
可用於從給定的初始值產生值
iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]
另一個有趣的函數是 Stream.resource/3
,可用於包裝資源,保證它們在列舉之前正確開啟,並在之後關閉,即使在失敗的情況下也是如此。例如,File.stream!/1
建立在 Stream.resource/3
之上,用於串流檔案
iex> stream = File.stream!("path/to/file")
%File.Stream{
line_or_bytes: :line,
modes: [:raw, :read_ahead, :binary],
path: "path/to/file",
raw: true
}
iex> Enum.take(stream, 10)
上面的範例將擷取您選取檔案的前 10 行。這表示串流對於處理大型檔案或網路資源等緩慢資源非常有用。
Enum
和 Stream
模組提供了廣泛的函數,但您不必全部記住。熟悉 Enum.map/2
、Enum.reduce/3
和名稱中包含 map
或 reduce
的其他函數,您將自然而然地建立起對最重要使用案例的直覺。您也可以先專注於 Enum
模組,僅在需要惰性的特定場景中才轉移到 Stream
,以處理緩慢資源或大型、可能是無限的集合。
接下來,我們將探討 Elixir 的核心功能,Process,它允許我們以簡單易懂的方式編寫並行、平行和分散式程式。