yu-tarrrrの日記

完全に個人の趣味でその時々書きたいことを書く

ElixirのEnumをそれなりに使いこなしたい

はじめに

  • 今回のブログはElixirのEnum.XXを使いこなせるようになりたいという願望です
  • きっかけはプロダクションコードで、配列操作をするときにEnum.mapで力でねじ伏せることが多くなり、もっとスマートにもっとElixirらしくかけたらいいのにという思いが生まれたことです。 例えば、Enum.filter()Enum.find()の違いを知っておくことでコードの仕上がりもだいぶ変わってきます
  • なので、今回は知っていれば個人的に役立ったEnum以下の物を記して行きたいと思います

Enum

  • そもそも、ElixirでいうEnumとはenumerables (数えられる物たち)を操作するためのアルゴリズムの集合体みたいな位置付けです
  • 一般的に使うenumerables としては、ListRangeMapが挙げられます

Enum.以下にあるもの

  • 雑にiexを起動して、Enum以下にあるものを取得してみます
Interactive Elixir (1.10.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Enum.
EmptyError           OutOfBoundsError     all?/1
all?/2               any?/1               any?/2
at/2                 at/3                 chunk_by/2
chunk_every/2        chunk_every/3        chunk_every/4
chunk_while/4        concat/1             concat/2
count/1              count/2              dedup/1
dedup_by/2           drop/2               drop_every/2
drop_while/2         each/2               empty?/1
fetch!/2             fetch/2              filter/2
find/2               find/3               find_index/2
find_value/2         find_value/3         flat_map/2
flat_map_reduce/3    frequencies/1        frequencies_by/2
group_by/2           group_by/3           intersperse/2
into/2               into/3               join/1
join/2               map/2                map_every/3
map_intersperse/3    map_join/2           map_join/3
map_reduce/3         max/1                max/3
max_by/2             max_by/4             member?/2
min/1                min/3                min_by/2
min_by/4             min_max/1            min_max/2
min_max_by/2         min_max_by/3         random/1
reduce/2             reduce/3             reduce_while/3
reject/2             reverse/1            reverse/2
reverse_slice/3      scan/2               scan/3
shuffle/1            slice/2              slice/3
sort/1               sort/2               sort_by/2
sort_by/3            split/2              split_while/2
split_with/2         sum/1                take/2
take_every/2         take_random/2        take_while/2
to_list/1            uniq/1               uniq_by/2
unzip/1              with_index/1         with_index/2
zip/1                zip/2
  • するとこれだけ、取得できました。これを全部見ていくと時間が無限に溶けていくと思うので、Enum — Elixir v1.10.4 ここをみてもらうようにして、今回は個人的によく使うなと思った物のみにフォーカスを当てていきます。

Enum.flat_map

  • 基本的には、他の言語にもあると思うのですが配列の中に配列があるようなケースで、最終的にflattenしたいような場合にこれを使うといいと思います。
  • 元々は、Enum.map -> Enum.concatの流れでやっていたのですが、Enum.flat_mapすれば一発で終了します
# 配列の中に配列があるような物に対して処理をしたいとき
iex(4)> enumerable = [[1,2,3] ,[4,5,6]]
[[1, 2, 3], [4, 5, 6]]
# こんな感じでアクセスして、最後にconcatしてフラットにしてました
iex(5)> Enum.map(enumerable, fn array -> Enum.filter(array,fn item -> item != 0 end ) end) |> Enum.concat
[1, 2, 3, 4, 5, 6]
# それもflat_mapを使うことですっきりします
iex(6)> Enum.flat_map(enumerable, fn array -> Enum.filter(array,fn item -> item != 0 end ) end)
[1, 2, 3, 4, 5, 6]

Enum.find

  • Enum.filterとの使い分けをうまくできていなかったのですが、Enum.filter()は中のfunctionでtrueの物を全て返す、Enum.find()中のfunctionでtrueの物のうち1つ目のものを返し、見つからなければdafaultを返します
# 先ほどの配列の中から、それぞれの配列の中で0以外のものをとるときにfindの場合下記のようになります
iex(13)> Enum.flat_map(enumerable, fn array -> Enum.filter(array,fn item -> item != 0 end ) |> Enum.take(1) end)
[1, 4]
# ちょっと冗長な感じがしますが、これをEnum.findを使えばすっきりさせられます
iex(14)> Enum.map(enumerable, fn enumerable -> Enum.find(enumerable, fn item -> item != 0 end) end)
[1, 4]
# Enum.findの方が戻り値が1件と明確なので、配列の中から取り出されて帰ってくるのでflattenする必要もなくなります。ただ、該当するものがない場合はnilを返すのでそこは要注意です
iex(15)> Enum.map(enumerable, fn enumerable -> Enum.find(enumerable, fn item -> item == 0 end) end)
[nil, nil]
# この場合はfindの第2引数にdefault値を詰めればよさそうです
iex(16)> Enum.map(enumerable, fn enumerable -> Enum.find(enumerable,0, fn item -> item == 0 end) end)
[0, 0]

Enum.reduce

  • どの言語にもありますが、さらっとreduceについても触れておきます
  • 例えば、配列の中身を掛け合わせたい場合とかに使えます
iex(6)> Enum.reduce([1, 2, 3, 4], fn x, acc -> x * acc end)
24
# accを用意しておけば、それに加算していくみたいな処理もできる
iex(7)>Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
6
# strtingでも可能ですが、1件目から処理して先頭に追加されていくのでreverseする必要はあります
iex(8)>Enum.reduce(["h","e","l","l","o"], "", fn x, acc -> x <> acc end)
"olleh"
iex(9)> Enum.reduce(Enum.reverse(["h","e","l","l","o"]), "", fn x, acc -> x <> acc end)
"hello"

Enum.zip

  • これは結構便利なのですが、配列の中で複数の配列を持っているときにそれぞれの対応性を持たせたいときに使う関数です
  • それぞれの配列の要素の頭からtuppleにしていって組み合わせてくれます
  • 戻り値はtuppleの配列です。(配列がネストされていても勝手にflattenしてくれます)
iex(10)>Enum.zip([[1, 2, 3], [:a, :b, :c]])
[{1, :a}, {2, :b}, {3, :c}]
# 仮に要素数が一致してない配列の場合は、少ない方の要素数までをzipしてtuppleの配列にしてくれます
iex(11)> Enum.zip([[1, 2, 3, 4], [:a, :b, :c]])
[{1, :a}, {2, :b}, {3, :c}]
# また、1つの配列にしなくても2つの配列であれば渡すことは可能です
iex(12)> Enum.zip([1, 2, 3], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
# この場合はアリティが多いので怒られます
iex(13)> Enum.zip([1, 2, 3], [:a, :b, :c], ["foo", "bar", "baz"])
** (UndefinedFunctionError) function Enum.zip/3 is undefined or private. Did you mean one of:

      * zip/1
      * zip/2

    (elixir 1.10.3) Enum.zip([1, 2, 3], [:a, :b, :c], ["foo", "bar", "baz"])
# 配列でwrapしてあげればOK
iex(13)> Enum.zip([[1, 2, 3], [:a, :b, :c], ["foo", "bar", "baz"]])
[{1, :a, "foo"}, {2, :b, "bar"}, {3, :c, "baz"}]
# ちなみにこれは通るけど
iex(14)> Enum.zip([[1, 2, 3]])
[{1}, {2}, {3}]
# こっちは怒られる。アリティが1つの場合、配列であり、中身も配列であることを期待しているので
iex(15)> Enum.zip([1, 2, 3])
** (Protocol.UndefinedError) protocol Enumerable not implemented for 1 of type Integer
    (elixir 1.10.3) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.10.3) lib/enum.ex:141: Enumerable.reduce/3
    (stdlib 3.13) lists.erl:1338: :lists.foreach/2
    (elixir 1.10.3) lib/stream.ex:1175: Stream.do_zip/3
    (elixir 1.10.3) lib/enum.ex:3134: Enum.zip/1

まとめ

  • Enum.XXは他にもたくさんありますが、Enum.map, Enum.flat_map, Enum.zip, Enum.reduce, Enum.find あたりを知っておけば困らないと個人的には思います
  • あとは実際に書いてみて、こんなやつないのかなと探してみるといいかなと思いました