Elixir

Elixirのwith文の挙動

More than 1 year has passed since last update.

環境

  • Elixir 1.3.3

やったこと

Elixir 1.2から追加されたwith文の挙動が気になったので調べてみた。with文の公式ドキュメントはこちら

with "aaa" <- "aaa",
  do: true
#=> true
# with文内のパターンマッチが全部成功しているのでdo文が実行される

with "aaa" <- "aaa",
  do: "ok"
#=> "ok"
# with文内のパターンマッチが全部成功しているのでdo文が実行される

with "aaa" <- "bbb",
  do: "ccc"
#=> "bbb"
# with文内のパターンマッチが失敗した場合最後に失敗した`<-`の右式が評価される

with "aaa" <- "aaa",
     "bbb" <- "bbb",
  do: true
#=> true
# 複数式ある場合も同様に全パターンマッチの試行に成功するとdo文が実行される

with "aaa" <- "aaa",
     "bbb" <- "ccc",
  do: true
#=> "ccc"
# 複数式ある場合も同様に失敗した`<-`の右辺が評価される

with a = "aaa",     
  do: true
#=> true
# a
#=> nil
# 変数のスコープはwith文内のみに制限される

まとめると

  • 上から順番にマッチするか評価
  • 全てマッチした場合do:が実行される
  • マッチしない場合はその時点でのマッチしなかった右式の結果が返る
  • with文内での変数はスコープがwith文内に制限される

またElixir 1.3から追加されたwith文のelseとguardの挙動

with "aaa" <- "aaa"
do 
  true
else
  _ -> false
end
#=> true
# パターンマッチに成功しているのでdo文が実行される。elseが無い場合と特に変わりない。

with "aaa" <- "bbb"
do 
  true
else
  _ -> false
end
#=> false
# パターンマッチに失敗した時点での`<-`の右式がelse内でパターンマッチされる

with "aaa" <- "bbb"
do 
  true
else
  "bbb" -> false
  _ -> "ccc"
end
#=> false
# "bbb"がelse内で"bbb"と一致したため式全体としては`false`となる

with "aaa" when "aaa" == "bbb" <- "aaa",
  do: true
#=> "aaa"
# with文内でGuardも使える

with "bbb" <- "aaa" do
   true
 else
   "bbb" -> false
 end
#=> ** (WithClauseError) no with clause matching: "aaa"
# else内でもマッチしない場合WithClauseError Exceptionになる

まとめると

  • マッチに失敗したときの結果をelse内でマッチ出来る
  • パターンマッチなのでwith内でGuard式も使える
  • elseにマッチする条件がない場合WithClauseError Exceptionになる

使用例

case文のネスト

例えばこのようなcase文のネストを

case Map.fetch(%{a: 1, b: 2}, :a) do
  {:ok, data} -> 
    case data do
      nil -> false
      data -> data
    end
  :error ->
    false
end

withで書き直すと

with {:ok, data} when not is_nil(data) <- Map.fetch(%{a: 1, b: 2}, :a)
do
  data
else
  {:ok, nil} -> false
  :error -> false
end

となる。

パイプライン

パイプラインを組んで前の処理の結果を次に渡したい時、パイプラインが長くなってくると前の処理の結果をチェックするために関数の頭でエラーチェックをしてそれを次に渡して…ということをするため、少々見辛くなる。

Map.fetch(%{a: 1, b: 2}, :a)
|> process
|> process2

defp process(%{:ok, data}), do: data
defp process(%{:ok, nil}), do: false
defp process(:error), do: false

defp process2(false), do: false
defp process2(data) do
  Map.fetch(%{1: "a", 2: "b"}, data)
end

これをwith文で書き直すと

with {:ok, data} <- Map.fetch(%{a: 1, b: 2}, :a),
     {:ok, result} <- Map.fetch(%{1: "a", 2: "b"}, data)
do
  result
else
  {:ok, nil} -> false
  :error -> false
end

のようになる。
見やすくなった気がする。

まとめ

  • with文の挙動と簡単な使用例を確認した
  • 実はElixir本体やEctoのような代表的なライブラリでもwithはあまり使用されていないし、José Valim(Elixirの作者)もあまり使うところはないかもしれないと言っている。
  • with文は確かに便利だけど挙動がパイプラインほど明示的ではないし、取っ付きにくさはあるので使うところは見極めた方がいいかもしれない。ここぞというときに使えばハマると思う。
  • with文が必要なほどソースが込み入って来たらその時点で設計が悪いか実装を見直した方がいいのかもしれない。