Enumとは何か
(すでにEnum
についてご存知の方はこの章をすっ飛ばしてください)
公式ドキュメントより引用
Enum
Provides a set of algorithms to work with enumerables.
列挙型に対して使用可能なアルゴリズム
Python
などでstring型
の値に対して.replace()
や.split()
といった関数が呼び出せるようにEnum
が提供するアルゴリズム(関数)は第1引数に列挙型(enumerables)
を受け取り処理を実行します。
s_val = "I am Elixir boy" print(s_val.split(" ")) # ['I', 'am', 'Elixir', 'boy']
Elixir
# map: 第1引数に列挙型を受け取り、各要素に指定の操作を実行(ここでは要素を10倍に) Enum.map([1,2,3], fn n -> n * 10 end) |> IO.inspect() # [10, 20, 30]
列挙型には何があるのか
Elixir
のEnum
の公式ドキュメントの一番上の所に書かれているものを引用しました。
In Elixir, an enumerable is any data type that implements the Enumerable protocol.
Elixirでは列挙型は列挙可能なプロトコルのいくつかのデータ型です。
Lists ([1, 2, 3]), Maps (%{foo: 1, bar: 2}) and Ranges (1..3) are common data types used as enumerables:
リスト、マップ、レンジが列挙可能型に該当します。
(翻訳については誤訳、お許しください)
hexdocs.pm
つまりは以下の3つのデータ型についてEnum
は使用可能ということです。
- リスト([1, 2, 3])
- マップ(%{"a" => "apple"})
- レンジ(1..100)
# list Enum.map([1,2,3], fn n -> n * 10 end) |> IO.inspect() # [10, 20, 30] # map Enum.map(%{"a" => 1, "b" => 2, "c" => 3}, fn {_, v} -> v * 10 end) |> IO.inspect() # [10, 20, 30] # range Enum.map(1..3, fn n -> n * 10 end) |> IO.inspect() # [10, 20, 30]
Enumの基本: map, filter, reduce について
ほとんどの繰り返し処理はmap
, filter
, reduce
を組み合わせることで記述可能です。この3つの関数についてはPython
やJavaScript
に実装されている関数なので馴染みのある方も多いのではないかと思います。
map
map
に関しては先ほどすでにサンプルのコードを紹介しましたが、改めて。map
は列挙型の各要素に対して、第2引数で指定した処理を適応します。戻り値は必ず同じ要素数を持つリストです。map(データ型)
, range(データ型)
を第1引数にした場合にリストになっている事に先ほど気づかれたでしょうか。
data = Enum.map(1..10, fn n -> n end) # 要素を10倍 Enum.map(data, fn n -> n * 10 end) |> IO.inspect() # [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] # 偶数であればeven, 奇数であればoddに変換 Enum.map(data, fn n -> if rem(n, 2) == 0 do "#{n} is even" else "#{n} is odd" end end) |> IO.inspect() # ["1 is odd", "2 is even", "3 is odd", "4 is even", "5 is odd", "6 is even", "7 is odd", "8 is even", "9 is odd", "10 is even"]
filter
filter
は名前の通り、列挙型の要素を絞り込むためにあります。第2引数に条件式を与えることで、false
判定となる要素を削ぎ落とし、新たなリストを作成します。戻り値は第1引数で渡された列挙型と要素数が異なるリストになっています。
data = Enum.map(1..10, fn n -> n end) # 5より大きな値を除去 Enum.filter(data, fn n -> n < 5 end) |> IO.inspect() # [1, 2, 3, 4] # 偶数の値のみ抽出 Enum.filter(data, fn n -> rem(n, 2) == 0 end) |> IO.inspect() # [2, 4, 6, 8, 10]
reduce
reduce
に関しては奥が深い、やれることがいくらでもあるため、詳細は別の記事にまとめています。reduce
の最大の特徴は受け取った列挙型を任意のデータ型に変換出来るという点です。リストを文字列に変換したり、合計値を算出したり、key, value
を用いてmap型
に変換したりと、第1引数で受け取った列挙型がmap
やfilter
とは異なり必ずしもリストが戻り値になりません。reduce
の詳細は以下の記事を過去に書いていますのでご参照下さい。
reduce
は第2引数にアキュムレーターと呼ばれる戻り値の初期値となる値をセットします。
data = Enum.map(1..10, fn n -> n end) # 要素の合計値を算出(戻り値はintegerなので0を指定) Enum.reduce(data, 0, fn n, acc -> n + acc end) |> IO.inspect() # 55 # key, valueを持つmap型に変換。keyはnの数値, valueは配列の要素を10倍した数値 # Map.put()を用いて新たなkey, valueのpairを追加する Enum.reduce(data, %{}, fn n, acc -> Map.put(acc, n, n * 10) end) |> IO.inspect() # %{ # 1 => 10, 2 => 20, 3 => 30, 4 => 40, 5 => 50, # 6 => 60, 7 => 70, 8 => 80, 9 => 90, 10 => 100 # }
ちなみにreduce
を使うことでmap
とfilter
の実装も出来てしまいます。
map
data = Enum.map(1..10, fn n -> n end) # reduceを用いて作成したmap関数 my_map = fn enum, func -> Enum.reduce(enum, [], fn v, acc -> # 受け取った関数の戻り値をリストに結合 acc ++ [func.(v)] end) end # 作成した無名関数を実行(無名関数の実行 関数名.(引数)) my_map.(data, fn n -> n * 2 end) |> IO.inspect() # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
filter
data = Enum.map(1..10, fn n -> n end) # reduceを用いて作成したfilter関数 my_filter = fn enum, func -> Enum.reduce(enum, [], fn v, acc -> # 受け取った関数の結果がtrueの場合にリストに結合 if func.(v) do acc ++ [v] else acc end end) end # 作成した無名関数を実行(無名関数の実行 関数名.(引数)) my_filter.(data, fn n -> n < 5 end) |> IO.inspect() # [1, 2, 3, 4]
Enum
の基礎であるmap
, filter
, reduce
について触れたところでEnum
を使って、もう少し実用的な処理で遊んでみましょう。 一連の処理にはパイプライン演算子
という戻り値を次の実行関数の第1引数としてカリー化してくれる便利な演算子を使います。
# それぞれの関数の戻り値が次の関数の実行時に第1引数として渡される(めちゃくちゃ便利) Enum.map(1..10, fn n -> n end) |> Enum.filter(fn n -> n < 5 end) |> Enum.reduce(0, fn n, acc -> acc + n end) |> IO.inspect()
Enumで遊ぼう編
2次元リストの要素を1つだけランダムに取得した後に先頭要素を取得する
使い所はともかく、Enum
で遊ぶ基礎的な処理です。
# 2次元リストを作成 Enum.map(1..10, fn _ -> Enum.map(1..10, fn m -> m end) end) |> Enum.map(fn lst -> Enum.random(lst) end) # リストの要素をランダムに1つ取得 |> Enum.at(0) # 先頭要素を取得 |> IO.puts() # 5(実行するたびに違う結果になります)
この部分でリストinリストのデータを作成しています。
Enum.map(1..10, fn _ -> Enum.map(1..10, fn m -> m end) end) # [ # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], # : # : # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # ]
Enum.random()
はランダムに値を1つ習得する関数です。map型
の場合にはkey, value
のセットのまま取得されます。Enum.at()
に関しては多言語でいうところのindex番号
を指定して要素から値を取得する関数に該当するものです。
リストinマップのデータからB'zの曲を取り出してtitleとreleaseを「:」で結合して返す
lst_data = [ %{title: "熱き鼓動の果て", singer: "B'z", released: 2002}, %{title: "ワインレッドの心", singer: "安全地帯", released: 1984}, %{title: "ZERO", singer: "B'z", released: 1992}, %{title: "calling", singer: "B'z", released: 1997}, %{title: "さまよえる蒼い弾丸", singer: "B'z", released: 1998}, %{title: "あの頃へ", singer: "安全地帯", released: 1992}, %{title: "TIME", singer: "B'z", released: 2002}, %{title: "じれったい", singer: "安全地帯", released: 1988}, %{title: "love me, I love you", singer: "B'z", released: 1995}, %{title: "悲しみにさよなら", singer: "安全地帯", released: 1985}, %{title: "CHAMP", singer: "B'z", released: 2017}, %{title: "HOME", singer: "B'z", released: 1998}, ] # 一連の処理をパイプライン化 lst_data |> Enum.filter(fn %{singer: singer} -> # 歌手情報がB'zのrecordのみ抽出 singer === "B'z" end ) |> Enum.map(fn %{title: title, released: year} -> # 抽出したデータを文字列に変換 "#{year}: #{title}" end ) |> Enum.each(fn v -> IO.puts(v) end) # 結果を出力
実行結果
2002: 熱き鼓動の果て 1992: ZERO 1997: calling 1998: さまよえる蒼い弾丸 2002: TIME 1995: love me, I love you 2017: CHAMP 1998: HOME
僕はB'zと安全地帯が好きです。まぁ、その話は置いておいて...
Enum.each()
は各要素に対して指定の処理を実行するmap
に似たものですが、戻り値がリストではなく、:ok
というアトム型が戻り値になります。今回の場合は文字列を出力して終了するのが目的であっため、戻り値がリストであるmap
を使わずにEnum.each()
を仕様しています。
このような処理はAPIの戻り値や、http request
経由で何らかの情報がリスト内にオブジェクトで送られてくるようなケース、データーベースから取得したレコードデータに対して処理を適応したい場合など実務に沿ったものをイメージして作成してみました。一連の処理を気軽にパイプラインとして実行することが可能なのがElixir
の強力な所です。
リストを指定要素ごとに分割して合計をそれぞれ算出する
並行処理をする場合に近いことをやります。実際に合計を算出させるのは別プロセスであることが多いですが、今回は簡略化のため、同じプロセスの中でやってしまいます。ついでなので最小値と最大値の取得までやっていますが、Elixir
のスタイルガイド的にはパイプライン演算子
で無名関数を使うのはNGなので今回はパイプラインで一括で処理できるように止むなしとしました。
# 1 ~ 10000までの要素を持つリストを作成 Enum.map(1..10000, fn n -> n end) |> Enum.chunk_every(100) # 要素を100ずつ分解して2次元リストに |> Enum.map(fn lst -> Enum.sum(lst) end) # それぞれのリストの合計値を算出 |> (fn lst -> {Enum.min(lst), Enum.max(lst)} end).() # 最小値と最大値を取得 |> IO.inspect() # {5050, 995050}
Enum.chunk_every()
は列挙型のデータを指定の要素数ごとに分解してくれます。オプションを細かく設定することが出来るのですが、今回は割愛します。
Enum.map(1..10, fn n -> n end) |> Enum.chunk_every(2) |> IO.inspect() # [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
Enum.min()
とEnum.max()
に関しては見ての通りというか、想像の通りの結果が返ってきます。
data = Enum.map(1..10, fn n -> n end) IO.puts(Enum.min(data)) # 最小値 IO.puts(Enum.max(data)) # 最大値 # 1 # 10
指定数だけプロセスを立ち上げてメッセージを送信。受信した値を合計する
最後に少しElixir
に寄せた処理をEnum
を使って実装してみました。実際はTask
モジュールなどを使えばもっと、シンプルに記述することが出来るのですが、せっかくなのでEnum
とプロセス同士のmessage passing(メッセージパッシング )
を用いて、起動した各プロセスから実行結果を受け取り、メインのプロセスにて合計してみます。
# 送信元のプロセスのid sender = self() # 受信用のプロセスを起動(10回メッセージを受信する) receiver = spawn(fn -> nums = Enum.map(1..10, fn _ -> receive do {:ok, num} -> num _ -> 0 end end) # 10回メッセージを受け取った後に立ち上げたプロセスに返す send(sender, {:ok, nums}) end) # プロセスを10個立ち上げる -> プロセスのidの一覧をリストに保持 Enum.map(1..10, fn n -> # 立ちあげたプロセス内で受け取った値を10倍 spawn(fn -> res = n * 10 # 受信用のプロセスに10倍した値を送信 send(receiver, {:ok, res}) end) end) # 受信用のプロセスが受け取った値を受け取る # うまくいっていれば[10, 20, ...100]というリストが送られてくる receive do # 集計値を受け取り、合計値を算出 {:ok, nums} -> Enum.sum(nums) |> IO.puts() _ -> 0 end # 550
こんなところでしょうか。パイプライン演算子
を使ってゴリ押ししようかと迷いましたが、可読性が著しく低下するのでやめました。Elixir
で並行処理をする際にはEnum.map(1..10, spawn(fnuction))
というような記述が立ち上げるプロセス数も指定出来て便利です。今回はプロセス数は10個で固定ですが、引数経由で受け取るようにすれば拡張性は増すでしょう。
最後に
Enum
は非常に便利で、Elixir
を記述する内の半数程度ではEnum
を記述している気がします。適した関数を選択して、うまく処理することが出来れば、ほとんどの処理はEnum
とパイプライン演算子
のコンボで解決することが可能です。
ただ、時には「Enum
で上手く書けない!」と思い詰める時もあるかと思います。そんな時は再帰関数
を使ってみると上手く記述することが出来るかもしれません。例えばEnum.reduce()
ではアキュムレーターの値を1つしか保持させることが出来ませんが、任意で作成した再帰関数であれば引数の数はいくらでも拡張が出来るので複数のアキュムレーターを用意することが出来ます。
今回、扱った内容の多くはデファクトスタンダードの一冊である「プログラミングElixir」にて学ぶことが出来ます。第5, 6, 7, 10が参考になるかと思います。