Hatena::Grouperlang

檜山正幸のErlang未確認情報 RSSフィード

 | 

2009-03-13 (金)

例外で投げるReasonデータについて考える

| 18:04

だいぶ前に小さなモジュールcommon_errrorを書いた(最後に付ける)。動機やラショネール*1の説明をしておこう。

Erlangの標準ランタイムエラー

Erlangの標準ランタイムエラーについては次で書いている。

この記事のなかにある表の原典は、

再掲しておく。

番号 Reasonデータ 説明
1 badarg 引数の型が不正。
2 badarith 算術演算引数が不正。
3{badmatch,V} パターンマッチが失敗。値Vがマッチしない。
4 function_clause 関数呼び出しにおいて、マッチする関数節が見つからない。
5{case_clause,V} case式において、マッチする分岐が見つからない。値Vがマッチしない。
6 if_clause if式において、真となる分岐が見つからない。
7{try_clause,V} try式のof節において、マッチする分岐が見つからない。値Vがマッチしない。
8 undef 関数呼び出しにおいて、関数が見つからない。
9 {badfun,F} 関数Fに関してなにか不具合がある。
10 {badarity,F} fun式に対する引数の個数が食い違っている。Fはfun式の実体と引数を記述するタプル。
11 timeout_value receive after式のタイムアウト値が、整数でもinfinityでもない。
12 noproc 存在しないプロセスにリンクしようとした。
13 {nocatch,V} catchの外でthrow式を評価しようとした。Vはthrowしたターム。
14 system_limit システムリミットに到達した。システムリミットに関する情報は「Efficiency Guide」を参照。

Reasonデータが実際に投げられるデータだが、次のどちらかの形をしている。

  1. atom() (単一のアトム)
  2. {atom(), term()} (アトムと任意のタームのタプル)

アトムをErrId、付随するタームをHintと呼ぶことにする。この言葉を使ってあらためて述べれば:

  1. 形式1 ErrId : エラーの種別を表す単一のアトム。ランタイムエラーの一部や、POSIXエラーで使用される。
  2. 形式2 {ErrId, Hint} : エラーの種別を表す単一のアトムと、ヒントとなるタームの組。ランタイムエラーの一部で使用される。

この2つの形式をユーザーライブラリで使うのは勧められない。

ユーザーライブラリで使うReasonデータの形式

次がいいと思われる。

  • 形式3 {{Mod, ErrId}, (Hint|none)}

ErrId, Hintの意味は、標準ランタイムエラーと同じ。だが、エラーの種別を、モジュールMod(アトム)とエラー種別ErrIdの組 {Mod, ErrId}とする。ヒントが特にないときは、第二要素をnoneとする。ヒントがないときに {Mod, ErrId} としてしまうと、先の {ErrId, Hint} とパターンで区別ができなくなる。

例えば、user_dbというモジュールでalready_registeredというエラーが発生し、ヒント情報としてターム{"hiyama", "hiyama at chimaira dot org", male}を投げたいときは、次を実行する。

throw({{user_db, already_registered}, % エラー識別
       {"hiyama", "hiyama at chimaira dot org", male} % ヒント・ターム
      })

この例外をキャッチした側が人間可読メッセージを生成したいときは、次の関数common_error:format_error/1に、受け取ったReasonデータをそのまま渡す。

%% module  common_error

format_error(Reason = {{Mod, ErrId}, Hint}) when is_atom(Mod) ->
  try
    Mod:format_error(Reason)
  catch
    error:undef ->
      S = atom_to_list(ErrId) ++ 
        if 
          Hint =:= none -> 
            "";
          true -> 
            io_lib:format(" ~p", [Hint])
        end,
      lists:flatten(S)
  end;

common_error:format_errro/1 は、実質的に Mod:format_error(Reason) を呼び出すだけ。ここで、Modは例外の発生源となったモジュール(より正確な解釈は、下の「エラー識別子の解釈と指針」を参照)。

ユーザーライブラリ側のformat_error/1

次のような感じの関数を準備する。Mod:format_error/1は、入れ子になった文字リスト(iolist()型データ)を返してもいい。

%% module  user_db

format_error(_Reason = {{?MODULE, ErrId}, Hint}) ->
  case Hint of
    none ->
      do_format_error(ErrId);
    _ ->
      do_format_error(ErrId, Hint)
  end.

do_format_error(not_started) ->
  "manager process is not started".

do_format_error(illegal_data, Hint) ->
  io_lib:format("illegal data: ~p", [Hint]);
do_format_error(already_registered, Hint) ->
  io_lib:format("already registered: ~p", [Hint]).

武士道プログラミングにのっとり余計なことは書かない。エラー処理でエラーが起きる事態まで考えてもしょうがない、ランタイムエラーに任せよう。

エラー識別子の解釈と指針

エラー識別子{Mod, ErrId}のモジュール名部分Modは、ErrIdに対する名前空間を提供する。モジュールfooに属する関数がエラーを投げるとき、その識別子を必ず {foo, Id} にする必然性はない。{Mod, ErrId} のMod部分は、エラーのカテゴリーモジュール名で代用したものだから。

複数のモジュール foo, bar, baz に対して、単一の識別子zotを割り当てて、エラー識別子は {zot, Id}を使ってもよい。この場合、モジュールzotは、format_error/1を公開するだけで、他の関数を含まなくてもよい。Erlangでは、モジュールのグループをアプリケーションと呼ぶから、アプリケーションに対して1つのエラーカテゴリーを割り当てるのが妥当かもしれない。

アプリケーション名がAppのとき、App.erl にformat_error/1を用意し、実際のエラーメッセージなどはApp_errors.erlのような別モジュールにまとめるほうが管理しやすいだろう。

いずれにしても、common_errorとライブラリの利用者は、Reasonデータの詳細には無関係に、common_error:format_error/1 を呼び出して人間可読メッセージを手に入れることができる。

[追記]実際に使ってみると、面倒なのでエラーカテゴリーもたいてい ?MODULE にしちゃうな。複数のモジュールに共通したエラーがあるときは、例外を投げるモジュールとformat_errorを実装するモジュールが違う事態になるけど、それってけっこう大規模なときだろう。[/追記]

その他のReasonデータ形式

common_error:format_error/1 は、次の形式のReasonデータも処理できる。

  1. 形式4 {{Mod, ErrId}, (Hint|none), MsgStr}
  2. 形式5 {{Mod, ErrId}, (Hint|none), MsgStr, MsgArg}

これは、エラーを投げる側が、その場でメッセージ文字列を生成したいときに使う形式。滅多に使わないと思う。

メッセージ文字列の書き方

Erlangの文化と習慣では:

  1. 英語(自然言語)の文だけを含む。ヒント情報がその文に含まれていてもよい。
  2. 文の先頭を大文字にしない。
  3. 文末にピリオドも改行も付けない。
  4. その他、余計な装飾は付けない。
  5. 戻り値は iolist() 型(http://erlang.org/doc/man/file.html)でよい。

ランタイムエラー/POSIXエラーと、それに対するメッセージの実例は、次のソースコードを参照。

  1. stdlib/src/lib.erl のexplain_reason/5
  2. stdlib/src/erl_posix_msg.erl の message/1

エラーメッセージの例

実際のメッセージ文字列を挙げておく。こんなふうに書く。文頭の大文字化、ピリオドの付加なども、最終的な表示を作る際に加工することを前提に、「中間段階で余計なことはしない」という方針。まー、Erlangらしい。

stdlib/erl_posix_msg.erl :

message(e2big) -> "argument list too long";
message(eacces) -> "permission denied";
message(eaddrinuse) -> "address already in use";
message(eaddrnotavail) -> "can't assign requested address";
message(eadv) -> "advertise error";
message(eafnosupport) -> "address family not supported by protocol family";
message(eagain) -> "resource temporarily unavailable";
message(ealign) -> "EALIGN";
message(ealready) -> "operation already in progress";
message(ebade) -> "bad exchange descriptor";
message(ebadf) -> "bad file number";
message(ebadfd) -> "file descriptor in bad state";
message(ebadmsg) -> "not a data message";
message(ebadr) -> "bad request descriptor";
message(ebadrpc) -> "RPC structure is bad";
message(ebadrqc) -> "bad request code";
message(ebadslt) -> "invalid slot";
message(ebfont) -> "bad font file format";
%%
%% ... 途中省略 ...
%%
message(exfull) -> "message tables full";
message(nxdomain) -> "non-existing domain";
message(_) -> "unknown POSIX error".

"EALIGN" はさぼっているか、ちゃんと書くのを忘れているな。「その他のエラー」は "unknown XXXX error" と書くのがよろし。

参考

ランタイムエラー、POSIXエラーのフォーマット化に対しては、

  1. lib:format_exception/6, escript:format_exception/2(プライベート),
  2. shell:report_exception/4(プライベート)、
  3. file:format_error/1

モジュールごとにformat_error/1を準備する方法に関しては、次に記されている。

ここ(common_error)で使った方法は、ioモジュールとは若干異なっている(精神は同じだ)。

同じような方法は次でも使われている(これで全部かどうかは分からない)。

  1. kernal/src/file_io_server.erl
  2. syntax_tools/src/epp_dodger.erl
  3. stdlib/src/erl_lint.erl
  4. stdlib/src/erl_parse.erl
  5. stdlib/src/erl_scan.erl
  6. stdlib/src/ms_transform.erl
  7. stdlib/src/erl_tar.erl
  8. stdlib/src/beam_lib.erl

[追記]「無限大を含む整数算術」でcommon_error用にformat_errorを書いていたわ。[/追記]

common_errorモジュールのソース:

%% -*- coding: utf-8 -*-

%  %@doc 人間可読エラーメッセージの生成を支援する.
%% @docfile "common_error.edoc"

-module(common_error).
%-compile(export_all).
-export([format_error/1]).

%% 
%% @type error_reason() =   ErrId |
%%                         {ErrId, Hint} |
%%                         {{Mod, ErrId}, (Hint|none)} |
%%                         {{Mod, ErrId}, (Hint|none), MsgStr} |
%%                         {{Mod, ErrId}, (Hint|none), MsgStr, MsgArg}
%% where
%%  ErrId = atom(),
%%  Hint = any(),
%%  Mod = atom(),
%%  MsgStr = string(),
%%  MsgArg = [any()].
%%  これらは例外として投げるデータ。


%% @doc 例外として投げられたデータから、人間可読メッセージ文字列を生成する.
%% @spec (error_reason()) -> string()
format_error(Reason) when is_atom(Reason) ->
  atom_to_list(Reason);
format_error({ErrId, Hint}) when is_atom(ErrId) ->
  S = atom_to_list(ErrId) ++ io_lib:format(" ~p", [Hint]),
  lists:flatten(S);

format_error(Reason = {{Mod, ErrId}, Hint}) when is_atom(Mod) ->
  try
    Mod:format_error(Reason)
  catch
    error:undef ->
      S = atom_to_list(ErrId) ++ 
        if 
          Hint =:= none -> 
            "";
          true -> 
            io_lib:format(" ~p", [Hint])
        end,
      lists:flatten(S)
  end;
  
format_error({{_Mod, _ErrId}, _Hint, MsgStr}) ->
  MsgStr;

format_error({{_Mod, _ErrId}, _Hint, MsgStr, MsgArg}) ->
  S = io_lib:format(MsgStr, MsgArg),
  lists:flatten(S);
    
format_error(_Other) ->
  "unknown error".

*1:rationale、僕は、某氏の影響でこの言葉を口頭でしょっちゅう口にするけど、あまり一般的じゃないみたい。

JohnettaJohnetta2011/09/20 10:08There's a terrific aonumt of knowledge in this article!

idihtrvmidihtrvm2011/09/20 22:58Ae3uCQ <a href="http://olosettxchao.com/">olosettxchao</a>

sngxpjsngxpj2011/09/21 03:11ayOhXp , [url=http://zdziauunxamo.com/]zdziauunxamo[/url], [link=http://wtbahyieqovf.com/]wtbahyieqovf[/link], http://ozvgbmmbijrb.com/

ndincwbndincwb2011/09/25 02:04ON6kwd <a href="http://kcwkfgrvmnmy.com/">kcwkfgrvmnmy</a>

rjbiggyerjbiggye2011/10/03 22:37JsPlJf , [url=http://cqbwbbmunyno.com/]cqbwbbmunyno[/url], [link=http://fbfwknufklze.com/]fbfwknufklze[/link], http://qgontgbvollb.com/

MargarethMargareth2012/12/26 17:53Keep on wriitng and chugging away!

aeduguecaeduguec2012/12/28 17:05Tw7ofP , [url=http://yxsisfncoryo.com/]yxsisfncoryo[/url], [link=http://zdhwawfdimqa.com/]zdhwawfdimqa[/link], http://vvwqnyvdmkze.com/

hrrcbgrjvwhrrcbgrjvw2014/12/03 18:13majbwfsmboh, <a href="http://www.vaunvrflwj.com/">xopycnkhum</a> , [url=http://www.rxvsuexcfd.com/]xhxjttevzg[/url], http://www.ubizlzcddr.com/ xopycnkhum

 |