10-列舉型別和流

StraightDave發表於2014-09-14

10-列舉型別和流

列舉物件
積極vs懶惰

10.1-列舉型別

Elixir提供了列舉型別(enumerables)的概念,使用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模組為列舉型別提供了大量函式來變化,排序,分組,過濾和讀取元素。 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模組在設計時為了適用於不同的資料型別,所以它的API被限制為多資料型別適用的函式。 為了實現某些操作,你可能需要針對某型別使用某特定的模組。 比如,如果你要在列表中某特定位置插入一個元素,要用List模組中的List.insert_at/3函式。而向某些型別內插入資料是沒意義的,比如範圍。

Enum中的函式是多型的,因為它們能處理不同的資料型別。 尤其是,模組中可以適用於不同資料型別的函式,它們是遵循了Enumerable協議。 我們在後面章節中將討論這個協議。下面將介紹一種特殊的列舉型別:流。

10.2-積極vs懶惰

Enum模組中的所有函式都是積極的。多數函式接受一個列舉型別,並返回一個列表:

iex> odd? = &(rem(&1, 2) != 0)
#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。 該操作將會生成的中間結果是含有100000個元素的列表。 然後我們過濾掉所有偶數,產生又一個新中間結果:一個50000元素的列表。 最後求和,返回結果。

這個符號的用法似乎和F#中的不一樣啊...

作為一個替代,流模組提供了懶惰的實現:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

與之前Enum的處理不同,流先建立了一系列的計算操作。然後僅當我們把它傳遞給Enum模組,它才會被呼叫。流這種方式適用於處理大量的(甚至是無限的)資料集合。

10.3-流

流是懶惰的,比起Enum來說。 分步分析一下上面的例子,你會發現流與Enum的區別:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

流操作返回的不是結果列表,而是一個資料型別---流,一個表示要對範圍1..100000使用map操作的動作。

另外,當我們用管道連線多個流操作時:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

流模組中的函式接受任何列舉型別為引數,返回一個流。 流模組還提供了建立流(甚至是無限操作的流)的函式。 例如,Stream.cycle/1可以用來建立一個流,它能無限週期性列舉所提供的引數(小心使用):

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.cycle/1>
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,它可以用來包裹某資源,確保該資源在使用前開啟,在用完後關閉(即使中途出現錯誤)。--類似C#中的use{}關鍵字。
比如,我們可以stream一個檔案:

iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>
iex> Enum.take(stream, 10)

這個例子讀取了檔案的前10行內容。流在處理大檔案,或者慢速資源(如網路)時非常有用。

一開始Enum和流模組中函式的數量多到讓人氣餒。但你會慢慢地熟悉它們。 建議先熟悉Enum模組,然後因為應用而轉去流模組中那些相應的,懶惰版的函式。

相關文章