環境
- 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文が必要なほどソースが込み入って来たらその時点で設計が悪いか実装を見直した方がいいのかもしれない。