Erlangのビヘイビアとは、「だいたいインターフェースのようなもの」と説明できるでしょう。例えば、Javaと比較するなら:
Java | Erlang | |
---|---|---|
インターフェース | ビヘイビア | |
実装クラス | コールバックモジュール | |
implements Foo | -behaviour(foo) |
そうであるなら、ErlangのビヘイビアをJavaのインターフェースで表現できそうですが、さて、どうでしょうか。
Erlangのビヘイビアとその例
Erlangのビヘイビア定義には特別な構文はなくて、予約された名前の関数を定義するだけです(「Erlang実験室:ビヘイビアを自分で定義する方法」参照)。例えば、gen_serverビヘイビアは次のようになっています。
-module(gen_server).-export([behaviour_info/1]).
behaviour_info(callbacks) ->
[
{init,1},
{handle_call,3},
{handle_cast,2},
{handle_info,2},
{terminate,2},
{code_change,3}
];
behaviour_info(_Other) ->
undefined.
もう少し宣言ぽい疑似構文で書いてみると:
behaviour gen_server {
init/1;
handle_call/3;
handle_cast/2;
handle_info/2;
terminate/2;
code_change/3;
}
こう書いてみても、宣言されているのは、関数の名前と引数個数(アリティ)だけです。Javaのインターフェースのように、引数、戻り値、例外の型情報は一切ありません。そもそもErlangには型宣言機構がないのでしょうがありません。その代わり、ドキュメントで、型と振る舞いを記述します。
Erlang自体には型記法がありませんが、EDocユーティリティが型記法をサポートしています(「Erlangの型記法(Type Notation)」と「続・Erlangの型記法(間違い訂正)]に書いてあります)。その記法を使ってgen_serverビヘイビアを書いてみましょう。manページ(http://www.erlang.org/doc/man/gen_server.html)に忠実に書くと煩雑になるので、多少簡略化します(タイムアウト関係を除いています)。
@spec init(Args) -> Result
where
Args = term()
Result = {ok, State} |
{stop, Reason}
State = term()
Reason = term()@spec handle_call(Request, From, State) -> Result
where
Request = term()
From = {pid(), reference()}
State = term()
Result = {reply, Reply, NewState} |
{noreply, NewState} |
{stop, Reason, Reply, NewState} | {stop, Reason, NewState}
Reply = term()
NewState = term()
Reason = term()@spec handle_cast(Request, State) -> Result
where
Request = term()
State = term()
Result = {noreply, NewState} |
{stop, Reason, NewState}
NewState = term()
Reason = term()@spec handle_info(Info, State) -> Result
where
Info = term()
State = term()
Result = {noreply, NewState} |
{stop, Reason, NewState}
NewState = term()
Reason = normal | term()@spec terminate(Reason, State) -> ok
where
Reason = normal | shutdown | term()
State = term()@spec code_change(OldVsn, State, Extra) -> {ok, NewState}
where
OldVsn = Vsn | {down, Vsn}
Vsn = term()
State = term()
NewState = term()
Extra = term()
Javaのインターフェースとして書いてみる
gen_serverビヘイビアは全部で6個の関数宣言を持ちますが、そのうちの3つだけを対象にJavaのインターフェースにしてみましょう。次のようになります。
package genserver;public interface GenServerBehaviourSub {
public InitResult init(Object arg);
public HandlerResult handleCall(Object request, From from, Object state);
public HandlerResult handleCast(Object request, Object state);
}
別になんてことないように見えるでしょう。ところが、けっこう大変なんです。何が大変かというと、このインターフェースに出現する型 InitResultとHandlerResultを定義することです。
Erlangでは、第1要素がタイプタグ(弁別子(discriminator)とも呼ぶ)となっているタプルを多用します。例えば、{person, Name, Age, MailAddress}のような感じです。Erlangのレコードとは、このようなタグ付きタプル(tagged tuple)のことです。
このタグ付きタプルの型を、「または」を意味する「|」でつなげた型もまた多用されます。Result = {ok, State} | {stop, Reason} がその例です。
このような型は、弁別子付き共用体(discriminated union type)と呼ばれたりもしますが、Javaのような言語が苦手とする型です*1。「弁別子=タイプタグ」も使えるようにして、継承による型階層もなるべく整合的にしようと思うと、型システムの実現がけっこう大変なのです。
感想
型をきちんと定義して、コンパイル時チェックの恩恵を受ければ、プログラムの安全性はずっと増します。しかし、そのために型システムを作るのはそれなりに手間がかかります。たくさんの型に名前を付けるのは苦痛です。似たような定義を繰り返すのも嫌気がさします。
もちろんこれはトレードオフの話なんですが、Erlangのように、型システムは心に思い描くだけで、リテラルとパターンマッチでどんどん書き始めてしまうほうがラクチンな状況は確実にありますね。
やっぱり型推論でしょ、ってか。
*1:タプルが直積型であるのに対して、弁別子付き共用体は直和型になります。タプルの要素番号の代わりに名前を導入すると構造体ですが、弁別子に名前を使えるので、構造体の双対が弁別子付き共用体だとも言えます。