Elixir入門 10: EnumとStream
Tags: elixir, webdev, tutorial, programming
Published: 2018-11-20 01:20:42

---
title: Elixir入門 10: EnumとStream
published: true
description: Elixir公式サイトの許諾を得て「Enumerables and Streams」の解説にもとづき、加筆補正を加えてElixirのモジュールEnumとStreamについてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Enumerables and Streams](https://elixir-lang.org/getting-started/enumerables-and-streams.html)」の解説にもとづき、加筆補正を加えてElixirのモジュール`Enum`と`Stream`についてご説明します。

# Enum
Elixirは[列挙型](https://ja.wikipedia.org/wiki/%E5%88%97%E6%8C%99%E5%9E%8B)(enumerable)の考え方にもとづく[`Enum`モジュール](https://hexdocs.pm/elixir/Enum.html)を備えています。[`Enum.map/2`](https://hexdocs.pm/elixir/Enum.html#map/2)関数を使うと、第1引数のリスト要素に第2引数の関数で処理を加え、その戻り値の要素を納めた新たなリストが得られます。

```elixir
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`の関数はさまざまな列挙可能なデータ型に使える[多態性](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%95%E3%82%A3%E3%82%BA%E3%83%A0)をもちます。扱うデータは[`Enumerable`プロトコル](https://hexdocs.pm/elixir/Enumerable.html)を実装していればよいのです。

連番整数のデータは[`../2`](https://hexdocs.pm/elixir/Kernel.html#../2)でも表せます。[`Enum.reduce/3`](https://hexdocs.pm/elixir/Enum.html#reduce/3)は、各データを処理してひとつの値にして返す関数です。第1引数のデータを第3引数の関数で処理し、その戻り値がつぎのデータのコールバックに渡されます。第2引数は最初のデータのコールバックに渡される初期値です。

```elixir
iex> Enum.map(1..3, &(&1 * &1))
[1, 4, 9]
iex> Enum.reduce(1..3, 0, &+/2)
6
```

なお、`&+/2`は`+/2`を`&`演算子でキャプチャしています(「[Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)」「関数のキャプチャ」参照)。

`Enum`モジュールの関数の仕事は、データ構造の要素をすべて取り出して処理すること(列挙)にかぎられます。要素を差し込んだり、値を書き替えるには、そのデータ型にもとづく操作が必要です。たとえばリストのインデックスに値を差し込むのなら、`List`モジュールの[`List.insert_at/3`](https://hexdocs.pm/elixir/List.html#insert_at/3)をお使いください。


# パイプ演算子
パイプ演算子[`|>`](https://hexdocs.pm/elixir/Kernel.html#%7C%3E/2)は左オペランドの値を左オペランドの第1引数に渡します。なお、[`List.flatten/1`](https://hexdocs.pm/elixir/List.html#flatten/1)は、引数に渡したリスト内の入れ子を平坦化する関数です。

```elixir
iex> [1, [[2], 3]] |> List.flatten
[1, 2, 3]
```

関数の戻り値をつぎの関数の引数に渡す処理が重なると、関数が入れ子になります。

```elixir
iex> Enum.reduce(Enum.map(1..3, fn x -> x * x end), 0, fn(x, acc) -> x + acc end )
14
```

`|>`演算子はこうした場合に、左の出力を右に入力するという見やすい書き方にできるのです。つぎの例ではキャプチャ演算子`&`も併せて使いました。

```elixir
iex> 1..3 |> Enum.map(&(&1 * &1)) |> Enum.reduce(0, &+/2)
14
```

`Enum.reduce/3`は使い道の広い関数です。けれど、ただすべての要素を足せばよいときは[`Enum.sum/1`](https://hexdocs.pm/elixir/Enum.html#sum/1)が使えます。

```elixir
iex> 1..3 |> Enum.map(&(&1 * &1)) |> Enum.sum
14
```

なお、パイプ演算子`|>`を使うとき、関数に第2引数以降がある場合には、引数をかっこ`()`でかこんでください。たとえば、[`List.flatten/2`](https://hexdocs.pm/elixir/List.html#flatten/2)は第2引数のリストをつないだうえで平坦化します。このとき`()`を省くと、加えるように警告が示されます。コードに誤解を招きやすくなるからです。

```elixir
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`モジュールの関数はすべて先行処理です。たとえば、上のコードはつぎの処理と同じく、ひとつ目の関数が直ちに戻り値のリストを返し、そのデータからふたつ目の関数が結果のリストをつくります。つまり、関数の呼び出しがいくつも重なると、その数だけ途中経過のリストができるのです。さらにデータの要素数が膨大になれば、その負荷を考えなければならないかもしれません。

```elixir
iex> square = Enum.map(1..3, &(&1 * &1))
[1, 4, 9]
iex> sum = Enum.sum(square)
14
```

`Stream`モジュールは、`Enum`と同じように`Enumerable`プロトコルのデータを扱う関数が備わり、しかも遅延処理です。つまり、処理を求められるまで、データの取り出し(列挙)は行いません。


# Stream
[`Stream`](https://hexdocs.pm/elixir/Stream.html)は、複数の関数が組み合わせられる遅延処理の列挙型です。`Stream`は、列挙の処理がいくつ続いても、データから要素をひとつずつ取り出します。つまり、途中経過のリストはつくりません。

たとえば、`../2`で定めた連番整数は`Stream`です。`Stream`に対して`Enum`の関数を呼び出すと、データは遅延実行されます。なお、[`Stream.filter/2`](https://hexdocs.pm/elixir/Stream.html#filter/2)は、第2引数のコールバックが`true`を返す要素だけ取り出します。

```elixir
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`](https://hexdocs.pm/elixir/Stream.html#cycle/1)は引数の列挙型データから無限の`Stream`を返す関数です。`Enum`の列挙する関数に渡すと、処理が終わらなくなるので気をつけましょう。[`Enum.take/2`](https://hexdocs.pm/elixir/Enum.html#take/2)は、引数の数だけ取り出して処理します。

```elixir
iex> [1, 2, 3] |> Stream.cycle |> Enum.take(10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
```

[`Stream.unfold/2`](https://hexdocs.pm/elixir/Stream.html#unfold/2)の処理の流れは、`Enum.reduce/3`と似ています。ただ、コールバック間で受け渡すデータがタプル`{現在値, 集計値}`のかたちをとるのです。そして、第1引数は初期値になります。

数学の数列で第1項と第2項を決めて、それ以降の数を求める考え方です。コールバックに終了のための処理がないかぎり無限の`Stream`になります。たとえば、つぎのコードは[フィボナッチ数列](https://kotobank.jp/word/%E3%83%95%E3%82%A3%E3%83%9C%E3%83%8A%E3%83%83%E3%83%81%E6%95%B0%E5%88%97-615599)から10項を取り出してリストで返します。

```elixir
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`](https://hexdocs.pm/elixir/File.html#stream!/3)は、引数のパスから読み込んだファイルを`Stream`([`File.stream`](https://hexdocs.pm/elixir/File.Stream.html))にします。ストリーミングをはじめると、ファイルはElixirが自動的に開きます。そして、読み込み終わるか、失敗すると閉じられるのです。読み込まれたファイルが処理されると、行単位で要素に分けられたリストになります。

```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: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- Elixir入門 10: EnumとStream
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)