本稿はElixir公式サイトの許諾を得て「Enumerables and Streams」の解説にもとづき、加筆補正を加えてElixirのモジュールEnum
とStream
についてご説明します。
Enum
Elixirは列挙型(enumerable)の考え方にもとづくEnum
モジュールを備えています。Enum.map/2
関数を使うと、第1引数のリスト要素に第2引数の関数で処理を加え、その戻り値の要素を納めた新たなリストが得られます。
iex> Enum.map([1, 2, 3], fn x -> x * x end)
[1, 4, 9]
iex> Enum.map(%{2 => :one, 4 => :two}, fn({k, v}) -> {v, div(k, 2)} end)
[one: 1, two: 2]
Enum
モジュールには多くの関数があり、変換や並べ替え、グループ化、フィルタ、項目の取り出しなどに用いられます。Elixirの開発者にもっとも使われるモジュールのひとつです。Enum
の関数はさまざまな列挙可能なデータ型に使える多態性をもちます。扱うデータはEnumerable
プロトコルを実装していればよいのです。
連番整数のデータは../2
でも表せます。Enum.reduce/3
は、各データを処理してひとつの値にして返す関数です。第1引数のデータを第3引数の関数で処理し、その戻り値がつぎのデータのコールバックに渡されます。第2引数は最初のデータのコールバックに渡される初期値です。
iex> Enum.map(1..3, &(&1 * &1))
[1, 4, 9]
iex> Enum.reduce(1..3, 0, &+/2)
6
なお、&+/2
は+/2
を&
演算子でキャプチャしています(「Elixir入門 08: モジュールと関数」「関数のキャプチャ」参照)。
Enum
モジュールの関数の仕事は、データ構造の要素をすべて取り出して処理すること(列挙)にかぎられます。要素を差し込んだり、値を書き替えるには、そのデータ型にもとづく操作が必要です。たとえばリストのインデックスに値を差し込むのなら、List
モジュールのList.insert_at/3
をお使いください。
パイプ演算子
パイプ演算子|>
は左オペランドの値を左オペランドの第1引数に渡します。なお、List.flatten/1
は、引数に渡したリスト内の入れ子を平坦化する関数です。
iex> [1, [[2], 3]] |> List.flatten
[1, 2, 3]
関数の戻り値をつぎの関数の引数に渡す処理が重なると、関数が入れ子になります。
iex> Enum.reduce(Enum.map(1..3, fn x -> x * x end), 0, fn(x, acc) -> x + acc end )
14
|>
演算子はこうした場合に、左の出力を右に入力するという見やすい書き方にできるのです。つぎの例ではキャプチャ演算子&
も併せて使いました。
iex> 1..3 |> Enum.map(&(&1 * &1)) |> Enum.reduce(0, &+/2)
14
Enum.reduce/3
は使い道の広い関数です。けれど、ただすべての要素を足せばよいときはEnum.sum/1
が使えます。
iex> 1..3 |> Enum.map(&(&1 * &1)) |> Enum.sum
14
なお、パイプ演算子|>
を使うとき、関数に第2引数以降がある場合には、引数をかっこ()
でかこんでください。たとえば、List.flatten/2
は第2引数のリストをつないだうえで平坦化します。このとき()
を省くと、加えるように警告が示されます。コードに誤解を招きやすくなるからです。
iex> [1, [[2], 3]] |> List.flatten [4, [5]]
warning: parentheses are required when piping into a function call. For example:
foo 1 |> bar 2 |> baz 3
is ambiguous and should be written as
foo(1) |> bar(2) |> baz(3)
Ambiguous pipe found at:
iex:
[1, 2, 3, 4, [5]]
先行と遅延
Enum
モジュールの関数はすべて先行処理です。たとえば、上のコードはつぎの処理と同じく、ひとつ目の関数が直ちに戻り値のリストを返し、そのデータからふたつ目の関数が結果のリストをつくります。つまり、関数の呼び出しがいくつも重なると、その数だけ途中経過のリストができるのです。さらにデータの要素数が膨大になれば、その負荷を考えなければならないかもしれません。
iex> square = Enum.map(1..3, &(&1 * &1))
[1, 4, 9]
iex> sum = Enum.sum(square)
14
Stream
モジュールは、Enum
と同じようにEnumerable
プロトコルのデータを扱う関数が備わり、しかも遅延処理です。つまり、処理を求められるまで、データの取り出し(列挙)は行いません。
Stream
Stream
は、複数の関数が組み合わせられる遅延処理の列挙型です。Stream
は、列挙の処理がいくつ続いても、データから要素をひとつずつ取り出します。つまり、途中経過のリストはつくりません。
たとえば、../2
で定めた連番整数はStream
です。Stream
に対してEnum
の関数を呼び出すと、データは遅延実行されます。なお、Stream.filter/2
は、第2引数のコールバックがtrue
を返す要素だけ取り出します。
iex> range = 1..6
1..6
iex> odd = Stream.filter(range, &(rem(&1, 2) != 0))
#Stream<[enum: 1..6, funs: [#Function<40.58052446/1 in Stream.filter/2>]]>
iex> square = Stream.map(odd, &(&1 * &1))
#Stream<[
enum: 1..6,
funs: [#Function<40.58052446/1 in Stream.filter/2>,
#Function<48.58052446/1 in Stream.map/2>]
]>
iex> sum = Enum.reduce(square, 0, &(&1 + &2))
35
Stream
は、途中経過のリストはつくらず、列挙処理の手順を表します。そして、Enum
モジュールの関数に渡されたとき、もとのデータから項目をひとつずつ取り出すのです。Stream
は膨大なデータを扱うとき有効です。無限のデータでもつくれます。
Stream
モジュールは、Enumerable
プロトコルのデータを扱う多くの関数が備わり、戻り値はStream
です。また、指定にしたがったStream
をつくる関数もあります。たとえば、Stream.cycle/1
は引数の列挙型データから無限のStream
を返す関数です。Enum
の列挙する関数に渡すと、処理が終わらなくなるので気をつけましょう。Enum.take/2
は、引数の数だけ取り出して処理します。
iex> [1, 2, 3] |> Stream.cycle |> Enum.take(10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
Stream.unfold/2
の処理の流れは、Enum.reduce/3
と似ています。ただ、コールバック間で受け渡すデータがタプル{現在値, 集計値}
のかたちをとるのです。そして、第1引数は初期値になります。
数学の数列で第1項と第2項を決めて、それ以降の数を求める考え方です。コールバックに終了のための処理がないかぎり無限のStream
になります。たとえば、つぎのコードはフィボナッチ数列から10項を取り出してリストで返します。
iex> fibs = Stream.unfold({1, 1}, fn({n0, n1}) -> {n0, {n1, n0 + n1}} end)
#Function<64.58052446/2 in Stream.unfold/2>
iex> Enum.take(fibs, 10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
File.stream!/3
は、引数のパスから読み込んだファイルをStream
(File.stream
)にします。ストリーミングをはじめると、ファイルは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)
Stream
は大きなファイルやネットワークの重いリソースを扱うのに適しています。はじめはEnum
を使って慣れていくのがよいでしょう。遅延処理が必要になったり、重いリソースや膨大なデータを扱うようになったとき、Stream
の利用をお考えください。
Elixir入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
Top comments (0)