ElixirのコレクションをEnumで処理する際のエラーハンドリングを呼び出し側で行なう
はじめに
Elixirにはコレクションを列挙して処理するためのモジュールとして Enum
モジュールというのがありますが、列挙中にエラーが発生して、実行をやめたい場合のエラーハンドリングについてやり方を調べる機会があったのでそのメモを残しておきます。
Enun.reduce_while
結論から言うとEnum.reduce_whileを用いると可能になります。
Enum.reduce_whileは以下のインターフェースとなっています。
reduce_while(enumerable, acc, fun)
第一引数にコレクション、第二引数にアキュムレータの初期値、第三引数にコレクションに対して実行する関数を受け取ります。
また、第三引数の関数は以下の2つの形式のタプルどちらかを返すことが期待されます。
{:cont, acc}
{:halt, acc}
Enum.reduce_whileでは、コレクションをすべて消費するか{:halt, acc}
が呼び出されるまで実行を継続します。
アイディアとしてはこの{:halt, acc}
のacc
のところでエラー情報を返してやれば、Enum.reduce_whileを呼び出している側でエラーハンドリングを行なうことが可能となります。
実際に使ってみたほうがわかりやすいので、説明はこの辺にします。
使ってみる
環境
利用するElixirのバージョンは以下のとおりです。
$ elixir -v Erlang/OTP 23 [erts-11.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] Elixir 1.10.4 (compiled with Erlang/OTP 22)
プロジェクトの作成
mixでプロジェクトを作成します。今回は動作を確認するためのテストも記述します。Enum内でエラーを意図的に起こすために関数の呼び出しをMock化します。モックライブラリとしてはmockを利用します。
$ mix new enum_error_handling
mix.exs
に依存を追加し、mockをインストールします。
defp deps do [ {:mock, "~> 0.3.0", only: :test} ] end
コードを記述する
Enum.reduce_whileを使ったコードをいかに記述します。
defmodule EnumErrorHandling do alias EnumErrorHandling.DoSomething require Logger def hello_reduce_while(some_collection) do Enum.reduce_while(some_collection, {:ok, []}, fn value, {:ok, acc} -> case DoSomething.do_something(value) do {:ok, new_value} -> {:cont, {:ok, [new_value | acc]}} :error = err -> {:halt, err} end end) |> case do {:ok, new_collection} -> {:ok, Enum.reverse(new_collection) } :error -> Logger.error("something hannpend") :error end end end defmodule EnumErrorHandling.DoSomething do def do_something(value) do # エラーが発生する可能性がある何かの処理をする value end end
関数hello_reduce_while
ではリストを受け取り、リストのそれぞれの値に対して、do_something
を実行し、その結果を新たなリストとして返す関数です。
EnumErrorHandling.DoSomething.do_something/1
はエラーが起こる可能性がある処理で、エラーが起こった場合はEnum.reduce_whileの実行結果として{:halt, err}
のerr
が返され呼び出し側でエラーハンドリングを行なうことが可能となります。上記のコードではEnumの処理中にエラーが発生した場合呼び出し側の方でエラーログを出力し:error
を返すようなハンドリングを行っています。
それではこの関数を実行するテストを書いてみます。
コードを実行してみる
まずは正常系のテストを記述します。
defmodule EnumErrorHandlingTest do use ExUnit.Case, async: false import Mock test "success" do with_mock EnumErrorHandling.DoSomething, do_something: fn value -> {:ok, value <> ": did something"} end do some_list = ["value1", "value2", "value3"] assert {:ok, ["value1: did something", "value2: did something", "value3: did something"]} == EnumErrorHandling.hello_reduce_while(some_list) end end end
mockでEnumErrorHandling.DoSomething.do_something/1
の呼び出しの処理をモックしています。
処理は単純で受け取った文字列に対して、": did_something"
の文字列を追記しています。
このテストは実行すると成功します。
それでは次に異常系のテストを記述します。
期待するのははエラー時にエラーログを出力し、:error
のアトムが返されることです。
テストは以下のようになります。
test "failed" do with_mock EnumErrorHandling.DoSomething, do_something: fn value -> case value do "value2" -> :error _ -> {:ok, value <> ": did something"} end end do some_list = ["value1", "value2", "value3"] assert EnumErrorHandling.hello_reduce_while(some_list) == :error end end
こちらのテストも成功しログは以下のようになります。
$ mix test . 12:57:11.745 [error] something hannpend . Finished in 0.08 seconds 2 tests, 0 failures Randomized with seed 657765
"success"
のテストと同様に成功し、12:57:11.745 [error] something hannpend
のエラーログが出力されているのがわかります。
いい感じにハンドリングできてますね。