Elixir: with/1でパターンマッチングを扱う

[`with/1`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1)を用いると、複数の句についてパターンマッチングさせることができます。公式ドキュメントの例と説明を参考に、かみくだいてご紹介します。

# `case/2`でパターンマッチングを入れ子にする
たとえば、つぎのような処理を行いたいとします。

```elixir
iex> opts = %{width: 10, height: 15}
%{height: 15, width: 10}
iex> {:ok, width} = Map.fetch(opts, :width)
{:ok, 10}
iex> {:ok, height} = Map.fetch(opts, :height)
{:ok, 15}
iex> {:ok, width * height}
{:ok, 150}
```

この場合、パターンが一致しなければ`MatchError`になってしまいます。

```elixir
iex> opts = %{width: 10}
%{width: 10}
iex> Map.fetch(opts, :height)                
:error
iex> {:ok, height} = Map.fetch(opts, :height)
** (MatchError) no match of right hand side value: :error
```

`case/2`を入れ子にしてパターンマッチングさせれば、エラーを切り分けることができるでしょう[^1]。

```elixir
iex> opts = %{width: 10}
%{width: 10}
iex> case Map.fetch(opts, :width) do
...>   {:ok, width} ->
...>   case Map.fetch(opts, :height) do
...>     {:ok, height} -> {:ok, width * height}
...>     error -> error
...>   end
...>   error -> error
...> end
:error
```


# 複数の句を`with/1`でパターンマッチングさせる
複数の句をパターンマッチングさせるのが`with/1`です。`<-`の右辺が返す式の値を、左辺のパターンとマッチングさせます。そして、合致したらつぎの式に進み、合致しなかったときにはその戻り値をそのまま返すのです。入れ子が避けられて見やすくなります。

```elixir
iex> opts = %{width: 10}
%{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>   {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
:error
```

すべての句が一致すれば、返されるのは最後の値です。

```elixir
iex> opts = %{width: 10, height: 15, depth: 5}
%{depth: 5, height: 15, width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>   {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
{:ok, 150}
```


# `with/1`の構文
`with/1`のパターンマッチングにガードを加えることもできます。

```elixir
iex> opts = %{width: 10, height: "15"}
%{height: "15", width: 10}
iex> with {:ok, width} when is_number(width) <- Map.fetch(opts, :width),
...>   {:ok, height} when is_number(height) <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
{:ok, "15"}
```

`with/1`の中の句で用いられる変数は、その外とは別個に扱われます。また、`<-`を使わない句も含めて構いません。

```elixir
iex> with = nil
nil
iex> opts = %{width: 10, height: 15}
%{height: 15, width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>   double_width = width * 2,
...>   {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, double_width * height}
...> end
{:ok, 300}
iex> with
nil
```

`<-`を用いない句の振る舞いは、`with/1`以外の構文と同じです。

```elixir
iex> with :width <- :height, do: :ok
:height
iex> with :width = :height, do: :ok 
** (MatchError) no match of right hand side value: :height
```

関数のように、`with/1`の引数となる句をかっこでくくっても構いません。また、一致しなかった場合の戻り値は、`else`オプションで処理が加えられます。

```elixir
iex> opts = %{width: 10}
%{width: 10}
iex> with(
...>     {:ok, width} <- Map.fetch(opts, :width),
...>     {:ok, height} <- Map.fetch(opts, :height)
...>   ) do
...>     {:ok, width * height}
...>   else
...>     no_match -> {:error, no_match}
...> end
{:error, :error}
```

`else`ブロックでパターンマッチングに失敗すると、例外として`WithClauseError`が発生します。

```elixir
iex> opts = %{width: 10}
%{width: 10}
iex> with(
...>     {:ok, width} <- Map.fetch(opts, :width),
...>     {:ok, height} <- Map.fetch(opts, :height)
...>   ) do
...>     {:ok, width * height}
...>   else
...>     {:error, no_match} -> {:error, no_match}
...> end
** (WithClauseError) no with clause matching: :error
```

[^1]: この例では、`opts`の値に対して、`case/2`で直接`%{height: height, width: width} -> {:ok, height * width}`のパターンマッチングを行えばもっと簡潔に書けます。実際には、はじめの句で得たデータから、さらにつぎの句でパターンマッチングするといった複数データを扱う場合に用いられます。公式ガイド「MIX AND OTP」の「[with](https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#with)」で書かれているコードの`serve/1`がその例です。