このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

Erlang実験室:武士道と云ふは死ぬ事と見付けたり

Erlangでは、「死ぬこと=プロセスをクラッシュさせること」の解釈/意義/価値観が、他の言語とは随分違います。潔<いさぎよ>く死ぬことが推奨されていますが、これは責務の放棄とは違います。

内容:

  1. 事故や災害への対処は個人ではなくて企業や社会が行うべき
  2. 正常と異常のはざま
  3. 例外を使うのは例外的?
  4. 多プロセス並列プログラミングと例外
  5. 潔さと無責任は違う -- 武士道プログラミング

●事故や災害への対処は個人ではなくて企業や社会が行うべき

Erlangの書き方や文化で、なかなか馴染めないのが「異常時の処理を書かない」という方針です。

多くのプログラミング言語のコードでは、次のような分岐をしばしば見受けます。


if (正常条件) {
正常時の処理;
} else {
異常時の処理;
}

switch (値) {
case 正常な値_1 :
正常時の処理_1; break;
case 正常な値_2 :
正常時の処理_2; break;
// ...
default :
異常時の処理
}

Erlangでは、基本的に異常時の処理は書きません。


if
正常条件 ->
正常時の処理
end

case 値 of
正常な値_1 ->
正常時の処理_1;
正常な値_2 ->
正常時の処理_2;
%% ...
正常な値_n ->
正常時の処理_n
end

正常条件/正常値からはずれる事態が起きると、自動的にランタイムエラーが発生してプロセスが死にます。多くのプログラミング言語/ランタイムシステムでは、プロセスが死ぬのは大惨事ですが、Erlangの軽量プロセスが死んでもたいした騒ぎではありません。複数プロセスの協調互助体制を作っておけば、いくつかのプロセスが死んでもシステム全体としては正常動作を保証できます*1

つまり、異常事態への対処や回復処理を個々の関数やプロセスでは行わず、もっとマクロな組織構造/社会体制のようなものでセーフネットを構築するのです。アームストロングの言葉を引用しておくと:

Joe Armstrong, "defensive programming"
http://www.erlang.org/pipermail/erlang-questions/2003-March/007869.html

If you do *nothing* to your code you get a good diagnostic anyway:



コードに何も書かなければ、より良い診断メッセージが得られる。[檜山補足:余計なことを書くとロクなことはないから、止めれ。]



In C etc. you have to write *something* if you detect an error - in Erlang it's easy - don't even bother to write code that checks for errors - "just let it crash".

Cとかだと、エラーを検出したら何か書かなければならない。が、Erlangではハナシが簡単、エラーの検査や対処のコードをわざわざ書いたりはしない。かまわないから、プロセスをクラッシュさせればいいのさ。

●正常と異常のはざま

というわけで、引数、メッセージ、入力ストリームなどから想定外のデータを受け取ったときは、何もしないでアッサリ死ぬのがErlang流なのです。潔く死ぬコードを書くには、異常事態なんて起こらないと仮定して、正常処理だけをツラツラ書けばよいのです。

しかし実際には、「想定内のエラー」があります。例えば、なんらかの構文のパーザーを書くとします。入力ファイルの構文エラーが見つかったら死んでしまうのでは使いものになりません。漢<おとこ>らし過ぎてバカです。この場合、呼び側に構文エラー情報を伝えなくてはなりません。

では、パーズすべきファイルが開かないときはどうでしょう? これは異常事態と考えることもできますし、「よくある不都合」(死ぬこたぁない)程度に考えて、呼び側に伝えるという判断もあるでしょう。

既存のErlangプログラムの多くでは、次のような方針でエラーへの対処とエラー情報の伝達をしています。

  1. 想定外の異常事態はランタイムシステムに任せる(何も書かない)。
  2. たまたま、想定外の異常事態を自分で検出してしまったら、erlang:error/1(旧称は erlang:fault/1 廃止予定)ですぐにクラッシュする*2
  3. 想定内のエラーは、バリアント型の戻り値で伝える。

最後のバリアント型とは、正常値は {ok, Value}、エラー値は {error, Reason} というパターンです。「Erlang実験室:例外的値とバリアント型データ」を参照してください。

●例外を使うのは例外的?

ここからは、異常事態(不慮の事故、災害)ではなくて、想定内のエラー(予測される若干の不都合)について考えます。前節で述べたごとく、標準ライブラリを含め、{ok, Value} または {error, Reason} を戻す方式(以下、ok/error方式と呼びます)が圧倒的に多く使われています。

しかし、カールソン(Richard Carlsson)は、ok/error方式の弊害を指摘しています。

カールソンの言うことを検討してみると、実にそのとおりで、想定内のエラーはok/error方式ではなくて、throwで投げるべきなのです。にもかかわらず、標準ライブラリでさえok/error方式なのはなぜでしょう? なんと、Erlangのリリース10(最新版はリリース12、R12B-*)になるまで、まともな例外機構が存在しなかったのです。

昔からcatch式はあったのですが、これはLispのcatchに由来するもので、いわゆる例外というよりは、関数を超えてのgotoであり、大域脱出制御用のツールです。したがって、想定内のエラーの伝達にthrowを使うという発想にはならなかったわけです。

今後のErlangプログラミングでは、まともな例外機構、つまりtry式を使うべきでしょう。1個のプロセス内で走る逐次プログラミングに関しは、例外を使えばコードが改善されます。

●多プロセス並列プログラミングと例外

逐次プログラミングにおける例外の使用法やマナーは、他の言語と変わらないと思います。問題は、複数のプロセスを使う並列プログラミングです。プロセス間で異常事態を通知する方法にシグナルがありますが、これは「想定内のエラー」伝達には使えません。シグナルを発行できるのは死ぬときに限るからです。ちょっとマズイことがあったからといって、遺書を書いて死んでいては、(文字通り!)命がいくつあっても足りません。

結局、正常値も異常値もメッセージで伝えるしかないので、ok/error方式に戻ってしまいます。ok/error方式が蔓延<はびこ>るもうひとつの理由は、Erlang関数に、メッセージングをラップしただけのものが多いこともあるでしょう。

例題に次の関数を考えてみましょう。


find_data(Key) ->
gen_server:call(the_server, {find_data, Key}, 1000).

the_serverが存在しなかったり、タイムアウトが発生すると、exit例外が発生するので、基本的には、find_data/1を呼び出したクライアントプロセスが死にます。これは、不測の異常事態と考えていいでしょう。

では、Keyに対応するデータが見つからなかったときはどうでしょう。the_server側に実装されたfind_dataが、ヤケになってexitなどしてはダメです。サーバープロセスが死んでしまい、クライアントプロセスも巻き添えで死ぬでしょう(適切に予防してないと)。ちょっとしたことで責任放棄して、回りに迷惑をかけて自滅する、もー、これ最悪。

要求されたデータが見つからないとき、ローカル関数(単一プロセス内でのみ完結して動く関数)なら例外を投げるのもアリですが、プロセス間RPCでは、やはり{error, Reason}メッセージを返すことになります。

ただし、{error, Reason}を受け取った側が例外を発生させる方法はあります。


find_data(Key) ->
case gen_server:call(the_server, {find_data, Key}, 1000) of
{ok, Value} -> Value;
{error, Reason} -> throw(Reason)
end.

find_data/1を使う側は、それがローカル関数なのかメッセージングをラップしているのか分からないときもあるので、他の関数群とのバランスを考慮して、違和感のない使い勝手を提供するのが望ましいでしょう。

●潔さと無責任は違う -- 武士道プログラミング

「ジタバタしないで潔く死ね」というポリシーをうまく伝えるのはなかなか大変です。言いたいことは、個々の関数が不測の事態に対処して(この表現は語義的に矛盾しているが)ゴチャゴチャやってもしょうがない。それより、組織/体制の構造により頑健性を担保せよ、ってことです。

困ってしまうのは、若干の不都合や想定内のエラーの対処までも「書かなくていいんだ」というバカげた誤解です。そんなわきゃない! それは、単にやることやってないからダメなだけです。アームストロングも注意してますが、ユーザーインターフェースを経由してくるイベント(人間が起源)や、WebのPOSTデータなどを、検査なしで受け入れ、自分の都合に合わないと死んでしまうようなプログラムは、「潔く死ぬ」プログラムではありません。単にアホタレのダメプログラムです。

「潔く死ぬ」は、自分の使命はちゃんと果たそうとしたが、不測の事態には対処できなかったし、そもそも不測(予測不可能)なんだから準備もしてなかったということです。例えば、トラックの運転手が走行中に大地震にあって荷物を目的地に届けられなかった(死んでしまった)のは誰も責めないでしょう。が、面倒だから荷物を積み残したとか、前方不注意で民家に突っこんだ、とかだと許されません。

結局、やってもしょうがない対処を書かないためには、やるべきことは何なのかを強く意識する必要があります。「やるべきこと」は、役割、責務、使命とか呼んでもいいでしょう。そして、どこまでが予測できる範囲かを確定することです。想定内の(仕様で決められた)環境では責務を果たし、想定外の事態には一切言及しない(コードに書かない)。それが「潔く死ぬ」ことになります。

こういう方針は、Erlang以外のプログラミング言語でも通じるとは思いますが、Erlangでは、言語仕様/ランタイムシステムが「潔く死ぬ≒武士道」的プログラミングを推奨/サポートしています。

*1:その方法は「OTP設計原理」としてまとめられ、OTPライブラリによりサポートされています。

*2:erlang:error/2 もあります。