例外

再度、契約による設計と例外について

More than 1 year has passed since last update.

以前、例外 Advent Calendar 2014という意欲的なアドベントカレンダーがあったのですが、残念ながら途中で頓挫することになってしまいました。これは、先日友人が訪ねてきたときに「そういえばそんなのもあったね」という感じで話したところ、なかなか面白そうだったので適当にまとめたものです。元のアドベントカレンダーの記事(契約による設計から見た例外)も参考にしてください。

契約による設計とは

契約による設計とは、端的に言えば「お前が俺の要求する値を俺に渡せば、俺がうまいことして、お前にこういう値を返してやる」ということを考えながら設計するとわかりやすいですよ、という考え方です。これはなかなか広範囲に適用できる便利な考え方であるにもかかわらず、あまりよく理解されていないどころか、防御的プログラミングと誤解されることもある不遇な考え方です。1

これは「考え方」なので2、防御的プログラミングのようなプログラミングテクニックと違い、ソースコードの上でどのように表わされるか(もしくは全く表されないか)はプログラミング言語やプログラマによってそれぞれです。Javaであれば、メソッドシグネチャだけで事前事後条件がわかるぐらい単純なメソッドでなら、それ以上ドキュメントを書かなかったりしますし、そうでなければJavadocに自然言語で書いたりします。

契約による設計という観点から他の技術を見ると、より物事が整理されて見えます。例えばデザインパターンや単一責任原則のようなプログラミングの原理原則などは「より良い契約の形とは何なのか」という方面の技術として見ることができます。関数型プログラミング、つまり「値によるプログラミング」は関数呼び出しに副作用が無く、契約が引数と戻り値の値によってのみ定義されるため、契約がわかりやすくなる傾向があり、「より良い契約の形」になりやすいです。防御的プログラミングは「信頼出来ない呼び出し側に対する、呼びだされ側の実行時の防衛手段」と捉えられます。静的型、特に定理証明支援系などは「契約をより正確に、機械可読な形で表現し、その契約が守られていることを、実行することなく静的に検査する」という技術と言えます。3

例外とは

例外とは「呼び出し側にも呼びだされ側にも特に非がないにもかかわらず、契約を遂行できない場合における制御の返し方」です。

「お前が俺の要求する値を俺に渡せば、俺がうまいことして、お前にこういう値を返してやる」という契約に基づき、関数呼び出しを受けたものの、やんごとなき事情により「俺がうまいことする」ができない場合があります。例えばファイルIOのAPIで「お前がファイルパスとバイト列を渡してくれたら、俺がファイルに書き込んで、お前に制御を返してやる」という契約に基づき引数を受け取ったものの、ディスクフルでもう書き込めません、というような状況です。呼びだされ側は「契約」により、「書き込んだら制御を返す」と言ったので、適当に制御を戻すわけにはいきません。これはもう呼び出し側にも呼びだされ側にも非がなく4、もうにっちもさっちもいかなくなります。例外はこのような不測の事態に用いられます。

「非がないにもかかわらず」や「遂行できない」という部分にはかなりの曖昧さがあります。契約をどのように設計するのかはプログラマ次第です。例えば、契約の条件に「ファイルの書き込みをしてから制御を戻すこと」が入っていれば例外が投げられますし、また「ファイルの書き込みができない場合は、-1を返す」という条件が入っていれば、例外を投げずに「正常に契約が完了した証」として-1を返します。どちらのスタイルを選ぶかは時と場合によります。5「より良い契約の形とは何なのか」という観点で言えば、「すべてに異常がない場合、例外機構が一切用いられずに正常にプログラムが動作するかどうか」が一つの基準になります。つまり、異常な場合にのみ例外を用いるようにして、正常な場合には例外を用いないようにする、ということになります。6

-1を「正常に契約が完了した証」と述べました。契約による設計において、例外が投げられずに正常に関数呼び出しが終わった場合、戻ってきた値はこの観点で言えば「正常値」です。-1nullOptional#empty()、これらはすべてなんとなく「異常値」のような感じがしますが、正常に呼び出しが終わって返ってきたので、それらは外れ値かもしれませんが、正常な値と捉えられます。7例えばつぎのような「ファイルからなんらかのユーザー情報を読みだして、オブジェクトとして返す」メソッドを考えましょう。

Optional<UserInfo> readUserInfoFromFile(File file) throws IOException;

このシグネチャは空かもしれないUserInfoが返ってくる、または例外が投げられる可能性があると述べています。ドキュメントが書かれていないので、詳細はわかりませんが、感覚として、ファイルIOでエラーがあった場合は例外が投げられ、エラーは起こらずにファイル読み込みはできたが、ファイルは空でユーザー情報がなかった場合は、Optional#empty()が返ってくる、という予測をすることができます。逆に次のようなシグネチャの場合を考えます。

UserInfo readUserInfoFromFile(File file) throws IOException;

今度はOptionalが返ってきませんので、ファイルが空の場合は例外が投げられるかもしれない、もしくは、UserInfoのNullObjectが定義されていて、それが返ってくるかもしれないという読み取り方をします。

この2つの例は「ファイルが空の場合」を正常な状態として捉えるかどうかについて、違う設計上の決断をしています。どちらを取るのかは「ファイルが空の場合」がアプリケーションにとって普通の状況なのか普通ではない状況なのかによって変わります。

メソッドシグネチャで投げられる例外が示されたように、例外も契約の一部と捉えるのが自然です。8呼び出し側が満たすべき事前条件、呼びだされ側が満たすべき正常に終わったときの事後条件と異常に終わったときの事後条件。この三つの組を一つとして契約ととらえたほうが考えやすいでしょう。

まとめとその他の話題

  • 契約とは、事前条件、正常時の事後条件、異常時の事後条件の三つ組から成る。
  • 特にシステム上なにも異常がない場合、契約の例外事項部分(=異常時の事後条件)は使わないほうがよい。
  • 契約条件をどのように設計するかはアプリケーションの要件次第。どのようにすれば使いやすくわかりやすい契約になるのかは、プログラマの腕次第。

より進んだ話題として、次のようなものが考えられます。

  • 事前条件が守られていない場合、どのようにすればいいでしょうか。例外を投げるとしたら、どのような例外を投げると良いでしょうか。
  • 例外が投げられたとき、呼び出し側はハンドルするかしないかを選択する必要があります。呼び出し側が例外をハンドルし、回復するためには、例外にどのような構造が必要でしょうか。また、回復できる例外とできない例外の違いは何でしょうか。例外の回復可能性とJavaの検査例外には、どのような関係があるでしょうか。ライブラリとアプリケーションで、例外の回復可能性はどのように変わってくるでしょうか。
  • 契約が守られているかどうかはどのように検査すればよいでしょうか。そもそも契約をうまくプログラムの上で表現し、言語処理系に契約の検査を行わせるためにはどうすればよいでしょうか。契約を型システムの上に表現し検査するとしたら、どのような契約はうまく検査でき、どのような契約は検査できないでしょうか。
  • 例外よりも柔軟な大域脱出を行う制御機構も存在します(例: setjmp,longjmp、call/ccや部分継続)。これらと契約による設計は、うまく適合するでしょうか。どのような形の契約がわかりやすいでしょうか。dynamic-windのような停止と再入を前提としたコードに対して、どのような契約を盛り込むべきでしょうか。

例外一つをとっても、さまざまな話題が考えられます。様々な主義主張を比較し、よりわかりやすく、より安全なプログラムを目指しましょう。


  1. Meyerが契約による設計を出した時代(1986年)は、ちょっと遡れば構造化プログラミング(1970年代)がやっと世に定着し、非構造化されたアセンブラから脱出しつつある、という時代だったのだと思います。契約による設計は、いささか現在では当たり前のように見えますので、現在ではすごく良く広まっている、とも言えるし、あまり明確に認識されていないという点では、あまり広まってないと言えるかもしれません。 

  2. 通常、考え方やパラダイムと特定の言語機構は区別します。例えば、「オブジェクト指向言語」と言った場合、正確には「オブジェクト指向というパラダイムをサポートする言語機構を持つ言語」ということになります。対して防御的プログラミングのようなプログラミングテクニックは、言語機構とは違い、プログラミング言語の上でよく使われるイディオムということになります。 

  3. これはあくまで「契約による設計」の観点から見ればそう捉えられる、というだけで、定理証明支援系やモデル検査のような形式手法は、プログラムの性質を検査する技術というふうに捉えるのが普通でしょう。 

  4. 呼びだされ側は請け負ったんだから責任持って書き込めと主張することはできますが、そんなことを言ってもどうにもならないものはどうにもならないわけです。 

  5. 例外機構が存在しない言語は、後者のスタイルが強制されます。 

  6. これはEffective Javaに書かれているはずですが、手元に本がありませんでしたので参照できません。 

  7. ここはかなり筆者のバイアスがかかっています。あくまで契約の観点から言えば正常ということです。 

  8. Meyerは例外を「契約を遂行できないときに使われるもの」として捉えていました。つまり正常な場合の事前事後条件のペアが契約で、その外に例外があるという形です。これは言葉の定義の問題です。