🚀

Elixirのforをおさらいしてみる

2020/12/09に公開
2

この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar 2020 9日目です。
前日は、@the_haigoさんの「Phoenix LiveViewで作る web/mobile チャットアプリ リアルタイム処理編」でした!

テーマ

ElixirではEnumモジュールを利用したリスト処理が鉄板ですが、OSSのコードを読んでいると意外とfor(内包表記)でリスト処理を書いているパターンがよく見られます。

例えばこちらとか。ネストしたリストのreduce処理をforでスッキリ書いています。
余談: zennのスクラップ機能でDashbit社のOSSをざっと見ていっていたときのログです。勉強になるコードが多いのでぜひ。

https://zenn.dev/koga1020/scraps/823045b9494296#comment-a8e102bd4ca19c

こんな具合に使いどころによってはforもありだなと。というわけで今日の記事では公式ドキュメントに沿って、改めてforの記述を整理してみようと思います。

https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1

バージョン

$ elixir -v
Erlang/OTP 21 [erts-10.3.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.11.2 (compiled with Erlang/OTP 21)

はじめに

Elixirのforは値を返します。ifなども同様ですね。そのため、forのなかでリストにpushして、、といった書き方はあまりせず、リストの各要素を処理(map)したり、リストからマップに集約(reduce)したりといった書き方が主になります。

forやifが値を返すというのはElixir入りたての最初の引っかかりポイントなので、念の為記載しておきます。

iex> arr = for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]
# arr に値が入っている
iex> arr
[2, 4, 6, 8]

リストの要素をループで回す

一番シンプルな例ですね。 n <- [1, 2, 3, 4] の部分はgeneratorと呼ばれるようです。

iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

ネストすることもできます。

iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]

条件でfilterする

rem/2は剰余を求める関数です。次の例ではrem(n, 2) == 0を満たす偶数のみが処理されます。

iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
[2, 4, 6]

Enumで書くとこんな感じでしょうか。forの方がスッキリしています。

# Enumで書くとこんな感じ
iex> [1, 2, 3, 4, 5, 6] |> Enum.filter(fn n -> rem(n, 2) == 0 end) |> Enum.map(fn n -> n end)

# &で書いてもこれぐらい
iex> [1, 2, 3, 4, 5, 6] |> Enum.filter(&(rem(&1, 2) == 0)) |> Enum.map(&(&1))

guardの構文と共にgeneratorを記述することでfilterを表現することもできます。

iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...>   String.upcase(name)
...> end
["JOHN", "MEG"]

シンプルな例だとこんな感じ。

iex> for x when is_atom(x) <- [1, "hoge", :hoge], do: x
[:hoge]

bitstringを処理する

bitstringもgeneratorで利用できます。

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
<<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

オプション

便利なオプションがいくつかあるので紹介していきます。

:into

Enum.into/2 相当の処理を:intoオプションで指定できます。

iex> for n <- ["1", "2", "3"], into: "", do: n
"123"
iex> Enum.into(["1", "2", "3"], "")
"123"
iex> for {key, value} <- [key1: :value1, key2: :value2], into: %{}, do: {key, value}
%{key1: :value1, key2: :value2}
iex> Enum.into([key1: :value1, key2: :value2], %{})
%{key1: :value1, key2: :value2}

:uniq

:uniqオプションで重複を除外できます。

iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]

:reduce

:reduceオプションでリスト処理の結果を集約できます。

iex> for n <- [1, 2, 3, 4], reduce: 0 do
...>   acc -> acc + n
...> end
10

もちろん、filterも同時に利用できます。

iex> for n <- [1, 2, 3, 4], rem(n, 2) == 0, reduce: 0 do
...>   acc -> acc + n
...> end
6

hexdocsの例がおもしろいですね。ちょっとややこしいので書き下しています。

# charlistとしてloop
iex> for <<x <- "AbCabCABc">>, do: x
'AbCabCABc'
# in句で小文字のa~zのみ抽出(filtering)
iex> for <<x <- "AbCabCABc">>, x in ?a..?z, do: x
'babc'
# reduceを追加して頻度をカウントする処理を記述
iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...>   acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}

まとめ

公式ドキュメントに沿ってforの使い方を整理しました。うまく使えば記述を減らせるタイミングがありそうですね。とくにguardでfilterしつつ:reduceはかなり利用シーンが多そうです。

明日10日目は@tuchiroさんの記事です!お楽しみに!

Discussion

zacky1972zacky1972

良いですね〜 forも使い方次第ですね。便利なオプションがいっぱいあるなあ。

koga1020koga1020

オプションを使いこなせるとグッと利用シーンが増えそうですね🚀