enumerableとstream

 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]

再帰

cf. 9 再帰 – Recursion – Elixir

Elixir では繰り返しを再帰で行う。例えば、文字列を n 回出力するスクリプトはこんな風になる。

defmodule Recursion do

  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end
  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end

end

Recursion.print_multiple_times("Hello!", 3)
^o^ > elixir recursion.exs
Hello!
Hello!
Hello!

リストの場合には [head|tail] と、終端条件には [] (空リスト)を使えばいい。

defmodule Math do

  def sum_list([], accum) do
    accum
  end

  def sum_list([head|tail], accum) do
    sum_list(tail, head + accum)
  end

end

IO.puts Math.sum_list([1, 2, 3], 0)
^o^ > elixir sum_list.exs
6

再帰についてはこれくらいでいいだろう。