Elixirのforをおさらいしてみる
この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar 2020 9日目です。
前日は、@the_haigoさんの「Phoenix LiveViewで作る web/mobile チャットアプリ リアルタイム処理編」でした!
テーマ
ElixirではEnumモジュールを利用したリスト処理が鉄板ですが、OSSのコードを読んでいると意外とfor(内包表記)でリスト処理を書いているパターンがよく見られます。
例えばこちらとか。ネストしたリストのreduce処理をforでスッキリ書いています。
余談: zennのスクラップ機能でDashbit社のOSSをざっと見ていっていたときのログです。勉強になるコードが多いのでぜひ。
こんな具合に使いどころによってはforもありだなと。というわけで今日の記事では公式ドキュメントに沿って、改めてforの記述を整理してみようと思います。
バージョン
$ 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
良いですね〜 forも使い方次第ですね。便利なオプションがいっぱいあるなあ。
オプションを使いこなせるとグッと利用シーンが増えそうですね🚀