[翻訳] Elixirのwithを学ぶ

Joseph Kainさんの2015年12月1日付のブログ記事[**Learning Elixir's with**](http://learningelixir.joekain.com/learning-elixir-with/)の翻訳です。

[**この記事**](http://qiita.com/HirofumiTamori/items/cdb9d2d46b638a38ae5a)でも書きましたがElixirはもうすぐ1.2がリリース予定(2015年12月10日現在v1.2.0-rc.0)で、いろいろと[**変更**](https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md)が入ります。

`with`は[**Language improvements**](https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#language-improvements)に含まれており

```
* Addition of the with special form to match on multiple expressions:
```
```ex
with {:ok, contents} <- File.read("my_file.ex"),
     {res, binding} <- Code.eval_string(contents),
     do: {:ok, res}
```

「複数の式にマッチさせるためにこのスペシャルフォームを追加した」とありますね。

-----

リリースが近づいてきた**Elixir 1.2**では新しいスペシャルフォーム `with`の導入が予定されています。本投稿でこの新しい特徴がどのように動作し、今後どのように効率的に使っていくとよいのかを調べてみます。

`with`は異なる構造化された結果を返すコマンドをまとめてチェーンするのを助けるために用意されました。特にひとつ使用例を挙げるとすればエラーハンドリングをちょっときれいに書くためです。あるコマンドの戻り値が`with`節とマッチしなかった場合、プログラムは全部の式を飛ばして`with`節を抜けます。この記事の下の方に書いた例でもう少し理解しやすくなるでしょう。

例に入る前に、私が今試しているのは`with`をサポートしている**Elixir**のマスターブランチであるということにご注意ください。とはいえ1.2リリースに向けてマスターブランチは既にクローズになっていますし、読者の方々もすぐに安定版リリースでこの機能を使えるようになるはずです。

## ドキュメントからの例

まず、私は`with`が実装済みですがまだ作業中のマスターブランチを使っていることを明記しておきます。正常かどうか確認するためElixirのドキュメントにある例をExUnitテスト用にコピーしてみます。

最初のテスト:

```ex
test "Example 1" do
  opts = %{width: 10, height: 15}
  assert {:ok, 150} ==
    with {:ok, width} <- Map.fetch(opts, :width),
         {:ok, height} <- Map.fetch(opts, :height),
      do: {:ok, width * height}
end
```

ここで`with`は2つの節をマッチしようとします(`<-`を含む2行がそれ)。`opts`が渡されて両方のマッチが成功し、`do`ブロックが実行されその結果を返します。ですので`with`式の結果は`{:ok, 150}`となり、つまりはアサーションは「真」となってテストが通ります。

```ex
test "Example 2" do
  opts = %{width: 10}
  assert :error ==
    with {:ok, width} <- Map.fetch(opts, :width),
         {:ok, height} <- Map.fetch(opts, :height),
      do: {:ok, width * height}
end
```

こちらは先の例を少し変更したものです。例1と同じ`with`式を持っていますが与えるデータが違います。このケースでは`:height`キー/バリューのペアが`opts`から抜けています。

[^1]: with式の2行めの方です、念のため。

`:height`がないので`Map.fetch/2`[^1]は`:error`を返し、`with`式は2番目の節のマッチに失敗します。`with`はマッチしない節に出くわすと評価を止めて右辺のマッチしない場合の値を返します。この場合は`:error`です。ですのでアサーションは「真」となりテストは通ります。

```ex
test "Example 3" do
  width = nil
  opts = %{width: 10, height: 15}
  assert {:ok, 300} ==
    with {:ok, width} <- Map.fetch(opts, :width),
         double_width = width * 2,
         {:ok, height} <- Map.fetch(opts, :height),
      do: {:ok, double_width * height}
  assert width == nil
end
```

ドキュメントからとった最後の例は`with`式のスコープについて示しています。

マッチングの挙動は例1と同じですが節は追加された値を計算します。

最初の節では`double_width`が計算されその`double_width`は`do`ブロック中でもスコープに入っています。

この例でもうひとつ。最初のマッチ節で変数`width`に値がバインドされていますね。しかしこれは`with`式のスコープの外にある変数`width`とは別のものです。つまり`with`式のスコープの内側でバインドされた変数はスコープの外側には影響しません。これは`for`スペシャルフォームと同じ挙動です。

### RFCの調査

`with`の機能は[Introducing withというRFC](https://gist.github.com/josevalim/8130b19eb62706e1ab37) [^2] で提案されています。RFCの内容をまとめてみましょう。

[^2]: Request For Comment コメント求む、仕様案を提示してコメントを求める。

`with`は`for`とは違ってコレクションから取り出された値ではなく値そのものにマッチします。簡単な例とそれよりもう少し複雑な例が書いてありますね。私はある一番気に入った例からネストしたcase文を簡単にするやり方を思いつきました。こんな感じです:

```ex
case File.read(path) do
  {:ok, binary} ->
    case :beam_lib.chunks(binary, :abstract_code) do
      {:ok, data} ->
        {:ok, wrap(data)}
      error ->
        error
    end
  error ->
    error
end
```

これを例えばこんな風に書けます:

```ex
with {:ok, binary} <- File.read(path),
     {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
     do: {:ok, wrap(data)}
```

`with`版の方が極めて簡潔で追いかけやすいと思います。ネストしたcase版はエラー処理のための余計で繰り返しの多い行が多いため、やりたいことがぼやかされてしまうのです。

### **with**で既存のエラー処理コードを置き換える

Elixirでうまくエラーを処理する方法を探していたので私は`with`シンタックスには注意を払ってきました。エラーチェックはよくできたパイプラインをぶち壊しにしてしまうのです。私が見つけた一つの方法としてはエラー値を表現するためにモナドを作成してそこにマップしてしまう方法です。

私が実際に作業している**Phoenix**ベースのプロジェクトでは、もっと簡単な方法を使うことにしました。チェーンの中の関数それぞれについて関数の頭の部分を分離させて書いたのです。これによりチェーンの中の関数はエラーをパイプラインの末尾まで送ります。これは簡単ですがちょっと長ったらしくなりますし、余分な関数の頭の部分は少々プログラムの意図をわかりにくくします。

こういうコードですね:

```ex
defp results(conn, search_params) do
  conn.assigns.current_user
  |> Role.scope(can_view: Service)
  |> within(search_params)
  |> all
  |> preload(:user)
end

defp within(query, %{"distance" => ""}), do: {:ok, query}
defp within(query, %{"distance" => x, "location" => l}) do
  {dist, _} = Float.parse(x)
  Service.within(query, dist, :miles, l)
end
defp within(query, _), do: {:ok, query}

defp all({:error, _} = result), do: result
defp all({:ok, query}), do: {:ok, Repo.all(query)}

defp preload({:error, _} = result, _), do: result
defp preload({:ok, enum}, field) do
  {:ok, Repo.preload(enum, field)}
end
```

プログラムの背景について少し説明します。

`results`関数は**Ecto**のクエリの合成で、`User`モデルを起点にそのユーザーが見ることが許可されているサービスを問い合わせます。次に指定された位置からの距離(distance)に含まれる集合に、それらのサービスを限定します。これはフォームのパラメータに基づいています。そしてサービスを取得しユーザーをプリロードします[^3]。

[^3]: どうやら動画配信のためのアンテナみたいなものを想定しているぽい？＞"service"。つまりあるユーザーが観る権利を持っているチャンネル(サービス)を発信しているアンテナが半径nマイル内に何本立ってるか？みたいな？？

`within`関数は`Service`の関数のためのラッパーです。この関数は距離(distance)を探さなくていい２つのケースを扱います。距離を探す必要がある場合はクエリを構築する作業を`Service.within`関数に受け渡します。この関数は失敗してもよいです。

`all`関数はエラー処理対応の一部です。もし`within`が`{:error,_}`を返したら、`all`はそのエラーをそのまま受け渡します。もし`within`が`:ok`と値を返したら`all`は`Repo`にクエリを投げます。

`preload`関数は`all`と同じようにエラーはそのまま受け渡し、エラーでない場合は`Repo.preload`を呼びます。

`with`を使うことでエラーの場合を取り除くことができました:

```ex
defp results(conn, search_params) do
  with user <- conn.assigns.current_user,
       query <- Role.scope(user, can_view: Orthrus.Service),
       {:ok, query} <- within(query, search_params),
       query <- all(query),
    do: {:ok, preload(query, :user)}
end

defp within(query, %{"distance" => ""}), do: {:ok, query}
defp within(query, %{"distance" => x, "location" => l}) do
  {dist, _} = Float.parse(x)
  Service.within(query, dist, :miles, l)
end
defp within(query, _), do: {:ok, query}

defp all(query), do: Repo.all(query)
defp preload(enum, field) do {:ok, Repo.preload(enum, field)}
```

改良された点:

* エラーを最後まで送るためだけに存在した余分な関数の頭の部分の必要がなくなった。
* `{:ok, term}`の形式をそれらの関数に渡す必要がなくなった。`with`で値が取り出せるようになった。
* `all`からの`{"ok, term"}`に含まれる結果をラップする必要がなくなった。単に通過させるだけになった。

`{:ok, term}`に含まれる`Repo.preload`の結果については`results/2`の戻り値だったためにラップする必要がありました。これを取り除くためには`results/2`を呼び出している側をなんとかしないといけませんし。

実際、もっと単純化を進めることができます。現時点で既に`Repo.all`までラップする理由はないでしょう。ですので`all`関数を取り除いて、さらにdoブロックの中でタプルを生成することにより`preload`関数も取り除くことができます。

```ex
defp results(conn, search_params) do
  with user <- conn.assigns.current_user,
       query <- Role.scope(user, can_view: Orthrus.Service),
       {:ok, query} <- within(query, search_params),
       query <- Repo.all(query),
    do: {:ok, Repo.preload(query, :user)}
end

defp within(query, %{"distance" => ""}), do: {:ok, query}
defp within(query, %{"distance" => x, "location" => l}) do
  {dist, _} = Float.parse(x)
  Service.within(query, dist, :miles, l)
end
defp within(query, _), do: {:ok, query}
```

もうひとつ、やらなくてもいいことをやってるところを改良できそうですね、`with`で。

```ex
defp results(conn, search_params) do
  with user <- conn.assigns.current_user,
       query <- Role.scope(user, can_view: Orthrus.Service),
       {:ok, query} <- within(query, search_params),
       query <- Repo.all(query),
    do: {:ok, Repo.preload(query, :user)}
end

defp within(query, %{"distance" => x, "location" => l}) do
  {dist, _} = Float.parse(x)
  Service.within(query, dist, :miles, l)
end
defp within(query, _), do: {:ok, query}
```

最初の`within/1`は最後の節とかぶっていたので取り除きました。

ここまできて結果に大満足です。コードは小さくなりましたし、個人的には意図がすごく追いかけやすくなったと思います。

## 結論

この投稿では皆さんといっしょに新しい`with`スペシャルフォームについて読んで、いじってみました。特徴とその利点について例を用いて説明しました。`with`を使っていくつかの既存コードをよりきれいで簡潔な形にリファクターしました。

私はこの新機能にワクワクしていると言わざるを得ません。私のプロジェクトをElixir 1.2にアップグレードして、コマンドのチェーンにおいてよりよいエラー処理をするため`with`の利点を活かしていこうと思います。