cf. 10 enumerableとstream – Enumerables and Streams – Elixir
enumerable
Elixir には enumerable (列挙できる)という概念がある。例えばリストとマップは enumerable だ。Enum モジュールには、enumerable なデータを変形、並び替え、グルーピング、フィルタリング、検索するための多くの関数がある。
iex(1)> Enum.map([1, 2, 3], fn x -> x * 2 end) [2, 4, 6] iex(2)> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end) [2, 12]
Enum モジュールの関数は enumerable なデータなら何でも受け付ける。上の例ではリストとマップを同じ関数が受け付けている。これを、ポリモーフィックという。Enum モジュールの関数は Enumerable プロトコルを実装したどんなデータでも受け付ける……「プロトコル」については後で出て来るらしいのでここではわきに置いておく。
Elixir には範囲もある。次の例では 1..3
が範囲だ。
iex(3)> Enum.map(1..3, fn x -> x * 2 end) [2, 4, 6] iex(4)> Enum.reduce(1..3, 0, &+/2) 6
貪欲vs怠惰
Enum モジュールの関数はすべて貪欲(eager)だ。enumerable なデータを受け取ってリストを返す。
……例を示す前にちょっとした準備。
iex(5)> odd? = &(rem(&1, 2) != 0) #Function<6.52032458/1 in :erl_eval.expr/5> iex(6)> Enum.filter(1..3, odd?) [1, 3]
さて、貪欲とは Enum モジュールの関数を連続して使ったとき、その操作ごとに中間リストを作ることを意味している。関数を連続して使うには |>
(パイプ演算子)を使う。
iex(7)> 1..100000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 7500000000
答えは出るけど、パイプごとに100000個(filter のあとは50000個)の要素を持つリストを生成している。
かわりに、Elixir には遅延操作できる Stream モジュールがある。Stream モジュールの関数は、中間リストを作る代わりに、Enum モジュールの関数に値を渡すときにだけ実行する一連の処理を作る。
iex(8)> 1..100000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) #Stream<[enum: 1..100000, funs: [#Function<46.64528250/1 in Stream.map/2>, #Function<39.64528250/1 in Stream.filter/2>]]> iex(9)> 1..100000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum 7500000000
上の例のように、Enum.sum
に渡す前の状態はリストではなく処理であって、Enum.sum
に値を渡すときになって初めて処理をする。これを怠惰(lazy)であるという。このへんは Haskell の遅延評価みたいなもんだな。
Stream
Stream モジュールの関数は、引数に enumerable なデータを受け付け、結果として stream を返す。
Stream モジュールには、結果が無限になるかもしれない関数もある。たとえば Stream.cycle/1
のような:
iex(10)> Stream.cycle([1, 2, 3]) |> Enum.take(10) [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]