はじめに
Elixir では、関数の戻り値として :ok
、:error
を含むタプルが返されることが多々あります。通常これらの値はパターンマッチングを利用して扱うことと思いますが、 :ok
などの記述が何度も繰り返されるのは見ようによっては冗長とも取れます。
この記事では、それら :ok
、 :error
を含む値の処理を簡潔に記述することができるライブラリ OK について紹介します。
:ok, :error を返す関数の処理
まずは、 OK
を使用しない通常のコードについて確認をしてみましょう。次のコードを見てください。
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
を追加してみます。
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
を実行した時の結果は以下のようになります。
iex(1)> Greeting.hello()
{:ok, "Hello, Bob, Jack, Hiroshi"}
もし、 Greeting.get_person2/0
が {:error, :not_found}
を返す場合は、 Greeting.hello/0
の実行結果は次のようになります。
iex(1)> Greeting.hello()
{:error, :not_found}
この関数の動作自体は特に問題にはなりませんが、 Greeting.hello/0
内の記述はタプルを多く含むため若干冗長な印象も受けます。
ここで OK
を使用すると、この処理をより簡潔に記述することができるようになります。
OK.forの使用
OK
を使うことで先のコードがどのように変化するでしょうか。
まずは下準備として、 mix.exs
の deps に OK
を追加します。
defp deps do
[{:ok, "~> 2.0"}]
end
これで OK
が使用できるようになりました。次に、 Greeting
に OK
を require して、コードを次のように変更しましょう。
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
です。次のコードを見てみましょう。
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 と同等の役割を果たすパイプであり、 :ok
と共に渡された値の処理を記述することができます。以下の例を見てみましょう。
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}
のタプルでラップされた値が ~>
で連結された関数の第1パラメータとして渡されて処理が行われます。また、いずれかの関数で {:error}
が返されれば、その値が結果となります。
~>>
~>>
は OK.flat_map/2 と同等の役割を果たすパイプであり、 ~>
と同様に :ok
と共に渡された値を指定された関数で処理します。 ~>
との違いは、処理した結果を :ok
のタプルでラップしない点にあります。先ほどの例を少し変更して、動作を確認しましょう。
defmodule Math do
use OK.Pipe
def calc() do
fetch() # {:ok, 5}
~>> double() # 10
end
# ...
end
最後に
この記事では OK
を使用することで :ok
、 :error
を含むタプルの処理を簡潔に記述できることを紹介しました。他にもいくつか便利な関数が提供されているので、詳細を知りたい方は ドキュメント を確認してみてください。