Blog ブログ

我儘は実装側の罪、エラー処理

こんにちは、プログラマーの安藤です

さて、前回は契約プログラミングのほんのさわりについて書きました。契約プログラミングは非常に強力な概念ですが、その成り立ちもあって残念なことにかなり理屈っぽい。

今回はそれに関連してエラー処理の話を理屈っぽく進めます。

ところで、一口にエラーといっても概念が大雑把すぎて理屈が進まない。まずは用語の定義。

失敗:
手続きが事後条件を満たさずに処理を終えること
正常終了:
手続きが処理を最後まで実行して終えること
異常終了:
手続きが処理を中断して呼び出し側に制御を戻すこと
では、二人の登場人物に出てきてもらいましょう、「手続きの呼び出し側」と「手続きの実装側」です。2者の間では事前に約束事が決められています。甲と乙の間の契約です。

  • 呼び出し側は手続きの事前条件を満たした上で手続きを呼び出します。事前条件を満たせない場合は、手続き側に正しい結果を要求する権利はありません。
  • 手続き側は事前条件が満たされているものとして仕事を進めます。仕事をやり遂げたあかつきには事後条件を満たした状態となり、これをもって呼び出し側の要求に応えたものとします。

はい、そこの理屈っぽいアナタ、こんな疑問が湧いてきませんか?

“仕事をやり遂げたあかつきには”だって?中途で仕事を放り出すことがあるのか?
それは実装側のバグじゃないのか?

残念なこと手続きが異常終了することはあるのです。そして実装側のバグだとも言い切れないのです。理不尽ですねえ。

ここでさらにもう一人の登場人物に出てきてもらいましょう。「手続きの外部サービス」さんです。

手続きの実装は、処理をすすめるうえで、さらに別の手続きを呼び出したり、OSのシステムコールを利用したり、I/O などのデバイスにアクセスしたりします。
これらをひっくるめて「外部サービス」と呼んじゃいましょう。だって、「手続き実装が呼び出すさらに別の手続き」じゃあ、ややこしくて話が進められない。

そして外部サービスが失敗することはあります。呼び出し側に落ち度が無いにもかかわらず。例えばファイルI/O。

「よし、ユーザーが指定したセーブ先のファイルが書き込みOPENできた、デバイスの残り容量も充分だ。WRITE」

ところが書き込み先のリムーバルストレージが引っこ抜かれたりするのです。嫌ですねえ。

と、いうわけで、どんなに正しくてバグのない処理でも失敗するときは失敗します
むしろ正しい処理をしたからこそ失敗するわけです。

だって、本当は失敗してるのに成功したふりをしてゴミをREADしてくるファイルI/O があったら嫌でしょ。

ではサービスを利用する側は、それが失敗したかどうかを判定することはできるでしょうか?

利用者に落ち度がないのにサービスが提供できなかった場合、失敗したことを利用者に伝える必要があります。

サービスが失敗した結果は、成功した結果とは区別できなければなりません。

当たり前ですねえ。この、正常時とは区別できる結果のことを、「いかなる正常なケースとも異なる」という意味で例外と呼びます。

今、私は理屈について話をしているのであって、特定の言語の機能についての話ではないってことをお忘れなく。例外を呼び出し側に伝える方法はいろいろあります。

  1. どのような正常な値とも異なる値を返す。
  2. 処理結果と一緒に終了コードを返す。
  3. 言語に組み込まれた例外機構を使って例外オブジェクトを throw する。
どの方法も一長一短。 c++ には、 try catch throw がありますが、「例外処理はすべてデストラクタで行われる」って思想の言語設計なもんだからなかなか導入できない。まだまだ「デストラクタってなに?それ美味しいの?」てなコードが溢れてる。

さて、手続きの実装が外部サービスを呼び出したはいいが結果が例外だった場合、できることといったら2つしかありません。

  • 進む
    • サービスの再試行など、手続きの事後条件を満たすための必要なものをそろえて手続きを最後までやり遂げる。
  • 戻る
    • 事後条件を満たす処理が継続できないので処理を中断して呼び出し側に手続きが失敗したことを示す結果(例外)を返す。

進むほうを選べるケースは限られています。サービス失敗の理由が「今は忙しいから後にして」っていう時ぐらいしかありません。大抵の場合は戻るほうを選ぶことになります。

果たして手続きが実行している途中で例外に遭遇した場合、その手続きの内部はどのような状態になっているでしょうか?

  • カウンターが辻褄の合わない数になっているかも知れない。
  • 手続きを正常終了したとき結果を収めるメモリをアロケートした状態かもしれない。
  • メモリ上に正常終了した結果を途中まで作成している段階かもしれない。

と、いった中途半端な状態になっていることでしょう。決してこのようなゴミをほったらかしにして呼び出し元に制御を戻してはいけません。広げかけた風呂敷はきちんと畳んで戻りましょう。

ゴミを入力にしては、どんなに正しい処理もゴミしか産みません。これが訳の分からない不具合の原因になる。怖いですねえ。

でっばっぐてぇのはゴミを生み出した処理を修正することであって、ゴミをなんとか食べられるように弄り回すことなんかじゃねぇんだよ!!!!”#$%&'()orz

失礼。取り乱してしまいました。

とにかく、エラーっていうのはこの中途半端なゴミのことです。そしてゴミを片付けることを、「例外によるエラーからの回復」といいます。実はエラーというのは呼び出し側の概念だったんですねぇ。

  • 失敗を報告しないのは実装側の罪
  • それを処理しないのは呼び出し側の罪
お後がよろしいようで。

エラーからの回復処置は、手続きが呼び出される前の状態まで巻き戻すことが望ましい。そうすれば呼び出し元のほうのエラー処理が簡単になります。

とはいえ、パフォーマンスやメモリ使用量の問題で完全に回復するのは難しいケースも多い。そんなときでも、少なくとも物事の辻褄があった状態を保たねばなりません。

さて、手続きの呼び出し元もまた、さらに外側の呼び出し元が利用するサービスを提供する手続きなわけでして、例外が発生した場合、それぞれの手続がエラー処理を実行して次々と外側の手続きへ連鎖してゆきます。

そして、メイン手続きが例外にであうとプログラムが異常終了する。理屈通り!

おっと、私達が作っているのは、ユーザーと対話的に処理するプログラムでした。最後に例外を処理するのはプログラムを利用しているユーザーってことになります。

はい、ダイアログ。
“セーブできませんでした。セーブ先デバイスのイジェクトが検出されました。別のデバイスにセーブしますか?”


採用情報

クラウドクリエイティブスタジオではプログラマを募集しております。
一緒に面白いゲームを作っていきましょう!