Enum
モジュールと|>
演算子
- このページの目標
Enum
モジュールに触れ、頻出するデータ操作を知っておく|>
演算子を知り、よくあるデータ操作のコードを読み書きできるようになる
- 所要時間: 15 分程度
資料: Enumerables and Streams - Elixir
- 関数型言語では「データと、それを扱う関数」がプログラムのほぼ全てであると前のページで書きましたが、 そのために Elixir は標準ライブラリの中に基本的なデータ操作モジュールを備えています
Enum
はその中でも代表的なもので、「数え上げられるデータ; Enumerable」を扱う関数を提供します- 数え上げられるデータとは、例えばリストや Map などです
- Enum – Elixir v1.9.4
- 以下で例に上げる
Enum.map/2
は非常に利用頻度の高い関数の 1 つです
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]
iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
1..3
は range 構文というもので、整数の連番リストを簡単に定義できます
- プログラミングを学んだことがあれば、言語にもよりますが、
for
文やwhile
文で配列等に対するループ処理を書いたことがあると思います - Elixir にはまず、
while
文が文法・標準ライブラリにありません。for
マクロはありますが、C 等のfor
文とはだいぶ違います - Enumerable な値の要素に対する処理は、
- 再帰関数として書くか、
Enum
モジュールの関数を使うか、for
マクロを使うか、が主たる方法です
- 再帰関数についてはRecursion - Elixirを参照
- 少し高度な内容なので、後回しで構いません
for
マクロは、いわゆる「内包表記; comprehension」と呼ばれるスタイルを実現するものです- Comprehensions - Elixirを参照
- 結構便利なのですが、これもまたちょっと高度な内容なので後回しで OK
Enum.map/2
は、対象とするデータと、対象とするデータの各要素に適用したい処理を関数として受け取ります- そして、受け取った関数を各要素に適用し、いわば変換したデータを返します
- このような、「関数を受け取る関数」を高階関数と言ったりします
- 「データと、それに対する処理」をより宣言的に書け、カウンタ変数を利用した記述と比べて off-by-one バグを生みにくい利点があります
- こういったデータ処理を関数の組み合わせで表現するのは関数型言語において基本的なスタイルと言えます
Enum.reduce/3
はもう一つの代表的な Enum 関数です。 ちょっと難しいですが見てみましょう
iex> Enum.reduce(1..3, 0, &+/2)
6
- 結果としては 1 から 3 までの整数を足し上げているのですが、以下のことが言えます
- 初期値(この場合
0
)を受け取っている - 前の要素に対する計算結果(この値を accumulator とよく呼ぶ)を次の要素の計算結果に利用して、最終的に 1 つの値を返している
- 無名関数を書く方法だけでなく、関数や演算子を
&name/n
記法で指定しても良い
- 初期値(この場合
- 少しわかりやすくすると、こうです
iex> Enum.reduce(1..3, 0, fn num, acc -> num + acc end)
6
- このような処理を「畳み込み」と呼ぶこともあります。言語によっては
fold
という名前で提供されています - 他にも
Enum
には便利な関数がたくさんあります。困ったらEnum
を探しましょう
|>
; The pipe operator
- 「1 から 10 万までの整数をそれぞれ 3 倍したもののうち、奇数であるものの合計値」を求める処理が書きたいとします
Enum
にはちょうどいい関数が揃っていますので、まず以下のように書けます
iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000
odd?
には省略記法で定義した無名関数が束縛されています。奇数を判定する関数ですねrem/2
は標準ライブラリが提供する剰余を求める関数です
100_000
は単なる整数ですが、読みやすさのためこのように 3 桁ごとに_
で区切る表記が推奨されていますEnum.map/2
に&(&1 * 3)
を与えて「各要素を 3 倍」し、Enum.filter/2
にodd?
を与えて奇数であるものだけを選び、Enum.sum/1
で合計しています
- しかし
()
が多くて読みにくい! そこで|>
(パイプ演算子; pipe operator)を使うと以下のように書けます
iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000
- 元データから、処理の順番そのままに、平坦に読めるようになりました
|>
は実体としてはマクロで、- 右辺の関数の第 1 引数を左辺に書き、
- 右辺の関数は第 1 引数を省略して書くことで、通常の関数呼び出しと同じことを実現できるようにするものです
- この演算子は別の関数型言語である F#(F Sharp)で登場し、データ処理の流れが読みやすく書けることから他言語にも波及しています
- 最初になにかデータがあり、そこに連続的に処理を適用していく場合には、このスタイルが読みやすく、頻出ですので覚えておきましょう