10 enumerableとstream - Enumerables and Streams

10.1 enumerable - Enumerables

Elixirはenumerable(列挙できる)という概念と,一緒に動かせるEnumモジュールを用意しています.私たちは既に2つのenumerableを学びました: リストとマップです.

Elixir provides the concept of enumerables and the Enum module to work with them. We have already learned two enumerables: lists and maps.

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のコードの中で頻繁に利用するモジュールの一つです.

The Enum module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code.

Elixirは範囲も提供しています:

Elixir also provides ranges:

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

ごらんの通りEnumモジュールは異なるデータ型でも動作するように考えられているので,そのAPIは様々なデータ型で使いやすいものだけになっています.あるデータ型固有の特徴的な操作はその固有のモジュールでやることになるでしょう.例えば,もしリストの特定の場所へ要素を挿入したい場合は,Listモジュールの関数List.insert_at/3を使うでしょう.たとえば範囲へ値を挿入するのはちょっと変な感じがしますもんね.

Since the Enum module was designed to work across different data types, its API is limited to functions that are useful across many data types. For specific operations, you may need to reach to modules specific to the data types. For example, if you want to insert an element at a given position in a list, you should use the List.insert_at/3 function from the List module, as it would make little sense to insert a value into, for example, a range.

様々なデータ型で動作するので,私たちはEnumモジュールの関数はポリモーフィックであると言っています.とりわけEnumモジュールにある関数はEnumerableプロトコルを実装したどんなデータ型でも動作するようになっています.後ほどプロトコルについて話しましょう,今はstreamと呼ばれるenumerableの一種をみていくことにします.

We say the functions in the Enum module are polymorphic because they can work with diverse data types. In particular, the functions in the Enum module can work with any data type that implements the Enumerable protocol. We are going to discuss Protocols in a later chapter, for now we are going to move on to a specific kind of enumerable called streams.

10.2 貪欲 vs 怠惰 - Eager vs Lazy

Enumモジュールの関数は全て貪欲です.たくさんの関数は一つのenumerableをとって一つのリストを返します:

All the functions in the Enum module are eager. Many functions expect an enumerable and return a list back:

iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]

つまりEnumで複数の操作をしたとき,結果が出るまで全ての操作が中間リストを作るということになります:

This means that when performing multiple operations with Enum, each operation is going to generate an intermediate list until we reach the result:

iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

上の例は操作をパイプラインでつなげています.まず範囲からはじまり,範囲の全ての要素を3倍します.最初の操作で100_000個の要素のリストができます.次にリストの中の全ての奇数を残すと,50_000個の要素をもった新しいリストができ,それから全ての要素を足し合わせます.

The example above has a pipeline of operations. We start with a range and then multiply each element in the range by 3. This first operation will now create and return a list with 100_000 items. Then we keep all odd elements from the list, generating a new list, now with 50_000 items, and then we sum all entries.

Elixirではかわりに,遅延操作できるStreamモジュールを用意しています.

As an alternative, Elixir provides the Stream module which supports lazy operations:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

中間リストを生成するかわりに,streamはEnumモジュールへ値を渡したときだけ実行される一連の処理を作ります.streamは大きかったり無限かもしれない集まりを扱うのに便利です.

Instead of generating intermediate lists, streams create a series of computations that are invoked only when we pass it to the Enum module. Streams are useful when working with large, possibly infinite, collections.

10.3 stream - Streams

streamはlazyで,composableなenumerableです.

Streams are lazy, composable enumerables.

上の例で観たように1..100_000 |> Stream.map(&(&1 * 3))1..100_000の範囲に対してmapを処理する表現であるstreamというデータ型を返すので,lazyです.

They are lazy because, as shown in the example above, 1..100_000 |> Stream.map(&(&1 * 3)) returns a data type, an actual stream, that represents the map computation over the range 1..100_000:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

さらに,複数のstream処理を通せるため,composableです.

Furthermore, they are composable because we can pipe many stream operations:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

Streamモジュールは引数へ全てのenumerableでも受けつけ,結果としてStreamを返します.無限になるかもしれないstreamを作る関数も用意しています.例えばStream.cycle/1は与えられたenumerableを無限に繰り返すstreamを作るのに使われます.永遠に繰り返すことになるので,こういったstreamからEnum.map/2のような関数を呼び出すことのないように気をつけてください:

Many functions in the Stream module accept any enumerable as argument and return a stream as result. It also provides functions for creating streams, possibly infinite. For example, Stream.cycle/1 can be used to create a stream that cycles a given enumerable infinitely. Be careful to not call a function like Enum.map/2 on such streams, as they would cycle forever:

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.cycle/1>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

一方Stream.unfold/2は与えられた初期値から値を生成するのに使われます:

On the other hand, Stream.unfold/2 can be used to generate values from a given initial value:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

その他の興味深い関数としてリソースを包むために使えるStream.resource/3があります.これはどこか途中で失敗したとしてもenumerationする前には正しく開かれ,終わったら閉じられることを保証します.例えば,私たちはファイルをstreamで扱うことができます:

Another interesting function is Stream.resource/3 which can be used to wrap around resources, guaranteeing they are opened right before enumeration and closed afterwards, even in case of failures. For example, we can use it to stream a file:

iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>
iex> Enum.take(stream, 10)

上の例はあなたが選んだファイルの最初の10行を取得します.このようにstreamは大きいファイルやネットワークのような遅いリソースを扱うのにとても使いやすいです.

The example above will fetch the first 10 lines of the file you have selected. This means streams can be very useful for handling large files or even slow resources like network resources.

最初はEnumStreamモジュールの関数や機能たちについて身構えてしまうでしょうが,回数を重ねるにつれだんだんと慣れていくでしょう.特に最初はEnumモジュールに集中して,遅いリソースや大きな(もしかしたら無限の)コレクションを扱うために遅延が必要であるものだけStreamにしていくことになるでしょう.

The amount of functions and functionality in Enum and Stream modules can be daunting at first but you will get familiar with them case by case. In particular, focus on the Enum module first and only move to Stream for the particular scenarios where laziness is required to either deal with slow resources or large, possibly infinite, collections.

次はElixirの中心的な特徴である,並行,並列,分散処理を簡単にわかりやすく簡単に書けるようにしてくれる,プロセスについて見ることにします.

Next we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way.