久々にErlangネタ。
Erlangにも、throw-try-catch方式の例外機構が備わっています。ですが、例外機構を備えた他の言語に比べると例外を使う機会が少ない気がします(印象、統計的裏付けはない)。ひとつの理由として、軽量プロセスの終了シグナルを使ったエラー処理が使えることがあるでしょう。また別な事情としては、例外ではなくて例外的値が非常に手軽に使えることが影響しているようです。
Erlangにおける、例外的値を使った処理
例えば、次のような仕様(構文は便宜上Java)の関数fooを考えます。
void foo(Object message) throws FooException;
Erlangだと、この程度のことなら例外を使わずに、エラーを示す値を使ってしまいます。そのような、例外的値を持つ関数仕様をElrangの記法で書くと次のようになります。
@spec foo(Message::any()) -> (ok | {error, Reason})
関数fooの具体例は*1:
foo(Message) ->
case whereis(foo_server) of
Pid when is_pid(Pid) -> Pid!Message, ok;
undefined -> {error, "foo_server is not registered."}
end.
この関数fooを使うときは、次のコーディングパターンを使います。
case foo呼び出し of
ok ->
正常時の処理;
{error, Reason} ->
異常時の処理
end.
例えば:
main() ->
case foo("hello") of
ok ->
ok;
{error, Reason} ->
io:fwrite("~p~n", [Reason]),
ng
end.
これは、次のtry-catch構文に相当します。
try {
foo呼び出し;
} catch(Exception e) {
異常時の処理;
}
正常時の処理;
やたらに自由度が高いErlang風バリアント型
今出てきた (ok | {error, Reason}) のような型は、他の言語では定義も扱いもかなり厄介なものです。厄介だから使うことも多くありません。ところがErlangでは、いたるところでパターンマッチングが出来るので、何の苦もなく (ok | {error, Reason}) を扱えます。扱えるだけではなく、便利なのです。
(ok | {error, Reason}) はErlang特有の“退化した”バリアント型です。まずは、もう少しわかりやすい例として、正常処理の結果が整数である関数を考えます。その仕様は次のようになるでしょう。
@spec bar(Arg::integer()) -> ({ok, Value} | {error, Reason})
where
Value = integer()
Reason = string()
barの戻り値型のスマートな定義は、多くのプログラミング言語にとって難題です(「Erlang実験室:ビヘイビアをJavaのインターフェースで書いてみた」の「Javaのインターフェースとして書いてみる」のところを参照)。IDL(Interface Definition Language)の共用体(ユニオン)型を使うと比較的素直に書けます。
enum ResultType {OK, ERROR};union Result switch(ResultType)
{
case OK: long value;
case ERROR: wstring reason;
};
バリアント型/ユニオン型は、値の集合で考えると、集合の直和に対応します。整数の集合をI、文字列の集合をSとして、それらの直和は I + S と書かれます。I + S は、{(t, x) | t∈{1, 2}, x∈I∪S} = {1, 2}×(I∪S) の部分集合として定義されるのが普通です。
- I + S = {(1, i) | i∈I} ∪ {(2, s) | s∈S}
もっと手短に書くと:
- I + S = {1}×I ∪ {2}×S
これは、「{1, 整数}という形のタプル、または {2, 文字列} というタプル」の集合を表します。味気ない数値タグ1, 2の代わりに2つのアトム ok, errorを使えば、対応する型は ({ok, integer()} | {error, string()}) ですね。共用体では、タプルの第二成分を取り出すのにより分かりやすいフィールド名value, reasonを使っているわけです。
もしvalueが整数型ではなくてvoid型なら、V={void}として、値の集合は V + S = {ok}×V ∪ {error}×S となります。Erlang記法では、({ok, void} | {error, string()}) ですが、{ok, void}というタプルは無意味なので、単なるアトムokにしてしまうと、(ok | {error, string()}) と“退化した”バリアント型になります。
*1:fooの定義に出てくるwhereisもアトムundefinedを例外的値として使っています。