LoginSignup
2
2

More than 5 years have passed since last update.

ElixirのGETTING STARTED(10. Enumerables and Streams)をやってみた

Posted at

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するために使える。

test.txt
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を使うようにすれば良いだろう。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2