Erlang/OTP 25.0以降ではmaybe
式が導入されているため、そちらを利用しましょう。本記事の対象読者は、それ以前のバージョンの利用者です。
概要
Erlangには他のプログラミング言語で言う所のreturn文がありません。
returnがないため、当然Early returnというテクニックが使えません。
Early returnは、異常が見つかった場合などに関数の早いタイミングで処理を打ち切ることができるため、ネストが深くなる前に関数を脱出することができます。
この記事ではEarly returnが使えないErlangでネストを深くしないためのテクニックついて説明します。
テクニック
1. case式よりも、多くの小さな関数
Bad
do_something(Arg) ->
case Arg of
a ->
do_a();
b ->
do_b()
end.
なるべく関数のパターンマッチで済ませましょう。
Good
do_something(a) ->
do_a();
do_something(b) ->
do_b().
when によるガードも同様です。
参考: inaka/erlang_guidelines#more-smaller-functions-over-case-expressions
2. まとめてパターンマッチ
Bad
do_something(A, B, C) ->
case do_a(A) of ->
ok ->
case do_b(B) of
ok ->
case do_c(c) of
ok ->
ok;
error ->
error
end;
error ->
error
end;
error ->
error
end.
最悪ですね。 タプルを使って以下のように書き直せます。
Good
do_something(A, B, C) ->
case
{
do_a(A)
do_b(B)
do_c(C)
{
of ->
{ok, ok, ok} ->
ok;
_ ->
error
end.
3. 関数分割
まとめてパターンマッチの例では do_*
関数の呼び出し間で依存関係がありませんでした。しかし、依存関係があった場合にはまとめてパターンマッチが使えません。
Bad
do_something(A, B, C) ->
case do_a(A) of ->
{ok, ResultA} ->
case do_b(B, ResultA) of
{ok, ResultB} ->
case do_c(C, ResultB) of
{ok, _ResultC} ->
ok;
error ->
error
end;
error ->
error
end;
error ->
error
end.
この例には関数分割を適用してみます。
Good
do_something(A, B, C) ->
case do_a(A) of ->
{ok, ResultA} ->
do_something_1(ResultA, B, C);
error ->
error
end.
do_something_1(ResultA, B, C) ->
case do_b(ResultA, B) of ->
{ok, ResultB} ->
do_something_2(ResultB, C);
error ->
end.
do_something_2(ResultB, C)
case do_c(ResultB, C) of ->
{ok, _ResultC} ->
ok
error ->
error
end.
${API関数名}_${N}
という命名法則によって元の大きな関数を分割することで、深いネストを解消できました。
関数一つあたりのスコープが小さくなってることも良いですね。
4. Errorリスト
実は、まとめてパターンマッチでのBadとGoodでは do_a/1
の error
時に do_b/1
を実行するかどうか(do_b/1
とdo_c/1
も同様)という違いがあります(関数分割も後続の関数は実行されせませんね)。
`まとめてパターンマッチでは全ての関数が実行されるため、それぞれがエラーだったかどうかを返せます。
do_something(A, B, C) ->
case
{
do_a(A)
do_b(B)
do_c(C)
{
of ->
{ok, ok, ok} ->
ok;
Other ->
{error, Other}
end.
1> do_something(A,B,C).
{error, {error, ok, error}}
% do_a/1 と do_c/1 が error
では、それぞれのdo_*
がエラーだったかどうかも返したいが、do_*
間で依存関係がある場合はどうすればいいでしょうか?
そんな時はErrorリストがいいでしょう.
Good
do_something(A, B, C) ->
Error0 = [],
{ResultA, Error1} =
case do_a(Param) of
{ok, ResultA} ->
{ResultA, Error0}
{error, Reason0} ->
[{error, Reason0} | Error0]
end,
{ResultB, Error2} =
case do_b(B, ResultA) of
{ok, ResultB} ->
{ResultB, Error1}
{error, Reason1} ->
[{error, Reason1} | Error1]
end,
{_ResultC, Error3} =
case do_c(C, ResultB) of
{ok, ResultC} ->
{ResultC, Error1}
{error, Reason2} ->
[{error, Reason2} | Error2]
end,
case Error3 of
[] ->
ok;
Error3 ->
{error, Error3}
end.
まとめ
Erlangのネストを深くしないために気をつけているテクニックとして
- case式よりも、多くの小さな関数
- まとめてパターンマッチ
- 関数分割
- Errorリスト
を紹介しました。油断すると一瞬でネストが深くなってしまう言語なので気をつけていきたいですね。
この記事中に出てくる用語は、筆者が適当につけたものですので、既に名前がありましたらコメントや編集リクエストでお教えください。
他にも有用なテクニックがあった場合にもご教授いただけると幸いです。