OKを使ってElixirの :ok, :error タプルをエレガントに処理

# はじめに
Elixir では、関数の戻り値として `:ok`、`:error` を含むタプルが返されることが多々あります。通常これらの値はパターンマッチングを利用して扱うことと思いますが、 `:ok` などの記述が何度も繰り返されるのは見ようによっては冗長とも取れます。

この記事では、それら `:ok`、 `:error` を含む値の処理を簡潔に記述することができるライブラリ [OK](https://github.com/CrowdHailer/OK) について紹介します。


# :ok, :error を返す関数の処理

まずは、 `OK` を使用しない通常のコードについて確認をしてみましょう。次のコードを見てください。

```elixir:greeting.ex
defmodule Greeting do
  def get_person1() do
    {:ok, "Bob"}
  end

  def get_person2() do
    {:ok, "Jack"}
  end

  def get_person3() do
    {:ok, "Hiroshi"}
  end
end
```

見ての通り、 `Greeting` module には人の名前を取得する関数が用意してあり、 `:ok`、`:error` を含むタプルとして値を返します。
さらに、 `Greeting` に上記の関数を利用して挨拶をする関数 `hello/0` を追加してみます。

```elixir:greeting.ex
defmodule Greeting do
  # ...

  def hello() do
    with {:ok, p1} <- get_person1(),
         {:ok, p2} <- get_person2(),
         {:ok, p3} <- get_person3() do
      {:ok, "Hello, #{p1}, #{p2}, #{p3}"}
    else
      err -> err
    end
  end
end
```

`Greeting.hello/0` を実行した時の結果は以下のようになります。

```sh:iex
iex(1)> Greeting.hello()
{:ok, "Hello, Bob, Jack, Hiroshi"}
```

もし、 `Greeting.get_person2/0` が `{:error, :not_found}` を返す場合は、 `Greeting.hello/0` の実行結果は次のようになります。

```sh:iex
iex(1)> Greeting.hello()
{:error, :not_found}
```

この関数の動作自体は特に問題にはなりませんが、 `Greeting.hello/0` 内の記述はタプルを多く含むため若干冗長な印象も受けます。
ここで `OK` を使用すると、この処理をより簡潔に記述することができるようになります。

# OK.forの使用

`OK` を使うことで先のコードがどのように変化するでしょうか。
まずは下準備として、 `mix.exs` の deps に `OK` を追加します。

```mix.exs
  defp deps do
    [{:ok, "~> 2.0"}]
  end
```

これで `OK` が使用できるようになりました。次に、 `Greeting` に `OK` を require して、コードを次のように変更しましょう。

```elixir:greeting.ex
defmodule Greeting do
  require OK

  def hello() do
    OK.for do
      p1 <- get_person1()
      p2 <- get_person2()
      p3 <- get_person3()
    after
      "Hello, #{p1}, #{p2}, #{p3}"
    end
  end
end
```

`with` の代わりに `OK.for` が追加され、同時に `:ok` を含めたタプルの記述が消えてスッキリしました。この場合でも `Greeting.hello/0` を呼び出した場合の実行結果は、変更前と同じになります。

コードの内容から大方検討がつくように、 `OK.for` は、失敗する可能性がある複数の操作をまとめるマクロです。 `{:ok}` の記述を省略して値の格納が可能となり、 `after` で記述した値が `{:ok}` でラップされて戻り値となります。`OK.for` 内のいずれかの関数で `{:error}` が返されれば、その値が `OK.for` の結果となります。

このように、 `OK.for` を使用することで、 `:ok` や `:error` の記述を省くことが可能となり、より本質的な処理に集中することができます。

それでは、 `OK` で使用可能な他のマクロについても見てみましょう。

# OK.try
`OK.for` では、 `{:error, _}` が関数から返された場合はその値をそのまま戻り値としていましたが、エラーの値に応じて処理をハンドルしたいケースも考えられます。その場合に便利なのが `OK.try` です。次のコードを見てみましょう。

```elixir:greeting.ex
defmodule Greeting do
  require OK

  def hello do
    OK.try do
      p1 <- get_person1()
      p2 <- get_person2()
      p3 <- get_person3()
    after
      "Hello, #{p1}, #{p2}, #{p3}"
    rescue
      :not_found -> {:ok, "Hello, there"}
      err -> {:error, err}
    end
  end
end
```

変更点としては、 `OK.try` を使用していること、そして `rescue` が加えられたことです。`rescue` では、 `:error` タプルと共に返された値に応じた処理を定義しています。このようにして、エラーに応じた処理を簡潔に記述することができます。

# パイプ
`OK` は独自のパイプも提供していて、これらを利用することで `:ok` を返す関数を簡潔に並べて記述することができます。

## ~>
`~>` は、 [OK.map/2](https://hexdocs.pm/ok/OK.html#map/2) と同等の役割を果たすパイプであり、 `:ok` と共に渡された値の処理を記述することができます。以下の例を見てみましょう。

```elixir
defmodule Math do
  use OK.Pipe

  def calc() do
    fetch()      # {:ok, 5}
    ~> double()  # {:ok, 10}
    ~> plus(3)   # {:ok, 13}
  end

  def fetch(), do: {:ok, 5}

  def double(a), do: a * 2

  def plus(a, b), do: a + b
end
```

このように、 `{:ok}` のタプルでラップされた値が `~>` で連結された関数の第１パラメータとして渡されて処理が行われます。また、いずれかの関数で `{:error}` が返されれば、その値が結果となります。

## ~>>
`~>>` は [OK.flat_map/2](https://hexdocs.pm/ok/OK.html#flat_map/2) と同等の役割を果たすパイプであり、 `~>` と同様に `:ok` と共に渡された値を指定された関数で処理します。 `~>` との違いは、処理した結果を `:ok` のタプルでラップしない点にあります。先ほどの例を少し変更して、動作を確認しましょう。

```elixir
defmodule Math do
  use OK.Pipe

  def calc() do
    fetch()       # {:ok, 5}
    ~>> double()  # 10
  end

  # ...
end
```

# 最後に
この記事では `OK` を使用することで `:ok`、 `:error` を含むタプルの処理を簡潔に記述できることを紹介しました。他にもいくつか便利な関数が提供されているので、詳細を知りたい方は [ドキュメント](https://hexdocs.pm/ok/readme.html) を確認してみてください。

# 参考
- [OK](https://github.com/CrowdHailer/OK)
- [Handling Errors in Elixir, No one say Monad.](http://insights.workshop14.io/2015/10/18/handling-errors-in-elixir-no-one-say-monad.html)
- [OK(HexDoc)](https://hexdocs.pm/ok/readme.html)