URL
試した環境
- Ubuntu Server 14.04 LTS
- Erlang/OTP 18
- Elixir 1.0.4
Enumerables
Elixirはenumerableという概念とそのEnumモジュールがある。
既にリストとマップの2つのenumeableを学んでいる。
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モジュールはenumerableなアイテムを変形、ソート、グループ化、フィルタリング、検索するための非常に幅の広い関数を提供する。Elixirのコードで頻繁に利用するモジュールの一つ。
Elixirには、範囲の表現も用意されている。
iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
Enumモジュールは異なるデータ型でも動作するように考えられている。APIは様々なデータ型で便利なものだけ制限されている。
特別な操作は、データ型で指定されるモジュールを使う必要がある。
例えば、リストの特定の場所へ要素をインサートしたい場合は、ListモジュールのList.insert_at/3
関数を使う必要がある。
様々なデータ型で動作するため、Enumモジュールはポリモーフィックと言っっている。特に、Enumモジュールの関数はEnumerableプロトコルをを実装したどんな型でも動作する。
次に、streamと呼ばれるenumerableの1つをみていく。
Eager vs Lazy
Enumモジュールの関数はeagerである。多くの関数はリストで返却する。
iex> odd? = &(rem(&1, 2) != 0)
#Function<6.54118792/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個のアイテムの新しいリストを生成する。最後に、それらの要素を足しあわせいる。
The pipe operator
上記のコードに使われている|>
記号はパイプ演算子である。これは単純に左側の式の出力を取得し、右側の関数に入力渡す処理をする。
Unixの |
演算子に似ている。この演算子の目的は、データが一連の関数によって変形される流れを強調することにある。
|>
演算子がどのようにコードを整理するか見るため、上記の例を |>
演算子を使わないで書き直すと次のようになる。
iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000
パイプ演算子についてはドキュメント参照
Streams
Enumの代わりとして、遅延操作をサポートするStreamモジュール
を提供している。
iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000
streamはlazy(遅延評価)で、composable(組み合わせ可能)なenumerablesである。
中間リストを生成する代わりに、streamはEnumモジュールへ渡した時にだけ呼び出される一連の処理を作成する。streamは大きかったり、無限かもしれないようなコレクションを扱う場合に有用。
上の例でみたたように、1..100_000 |> Stream.map(&(&1 * 3))
は1..100_000の範囲に対してmapを処理する表現であるstreamというデータ型を返すので,lazyである。
iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<47.72687021/1 in Stream.map/2>]]>
さらに、多くのstream演算子をパイプできるので、composableである。
iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000,
funs: [#Function<47.72687021/1 in Stream.map/2>,
#Function<51.72687021/1 in Stream.filter/2>]]>
Streamモジュールは引数にどんなenumerableでも受け付け、streamとして返却する。無限に可能性のあるstreamを作る関数も用意している。例えば、Stream.cycle/1
は、与えられたenumerableを無限に繰り返すstreamを作成することに使われる。このようなstreamで、Enum.map/2
のような関数を呼ばないように注意しなければならない。この繰り返しは永遠に終わらない。
iex> stream = Stream.cycle([1,2,3])
#Function<10.72687021/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<10.72687021/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]
その他の興味深い関数は、失敗の場合でもenumerationの前に正しく開き、終わったら閉じることを保証するリソースをラップするために使われるStream.resource/3
がある。例えば、ファイルをstreamするために使える。
Hello Elixir
iex> stream = File.stream!("test.txt")
%File.Stream{line_or_bytes: :line, modes: [:raw, :read_ahead, :binary],
path: "test.txt", raw: true}
iex> Enum.take(stream, 10)
["Hello Elixir\n"]
上の例は、選択したファイルの最初の10行を取得する。このようにstreamは大きなファイルやネットワークリソースのような遅いリソースを扱うのに大変役に立つ。
最初はEnumやStreamモジュールにある関数や機能の量は手強く感じさせるが
、その時々で慣れるだろう。最初はEnumモジュールに集中して、特に遅延が必要な遅いリソースや大きなリソース、無限の可能性があるコレクションを扱う場合をStreamを使うようにすれば良いだろう。