同じ内容を異なった方法で二回述べるようにすること。 Donald E. Knuth [1]
Jeffrey Richter氏による例外の定義から端を発した例外に関する一連の議論を見てきました。また、Bartrand Meyer氏による契約による設計という別の視点からも例外を見てきました。しかし、両者は同じ事を別の視点から言っているに過ぎません。両方の視点に立ち、概念を統合することで例外に対するより深い洞察を得ることができるでしょう。
その上で、契約そのものが持つ限界性に触れます。残念ながら、契約は例外における銀の弾丸とはなりえません。例外安全は例外における羅針盤となりえますが、具体的な方法論を提示するまでには至りません。例外に対する理論的かつ実践的な解決策は今のところありません。
例外的状況再考
初日にRichter氏の定義から、例外的状況を「ある命名された処理が、その名前から期待される処理を完了できなかった時」と定義しました。そして、契約による設計からも例外的状況を「契約が成り立たないように処理を呼び出した時」と定義できました。両者は同じことを言っています。
Richter氏の言う、「その名前から期待される処理」とは具体的にどのようなものでしょう。例えばStack
クラスのpop
メソッドは、その名前からどのような処理が期待されるでしょう。それを具体的に述べたものこそが契約です。Stack
クラスのpop
メソッドは「not empty?
な時にold(arr).length - arr.length == 1 and result() == old(arr).last
を満たすような処理」を期待できます。
つまり、正しさの条件とは処理の期待そのものの表現です。そして、両者の言い分を統合するならこうです。
例外的状況とは、契約が成り立たないように処理を呼び出したような状況であり、処理の事前条件や事後条件はその処理の名前に著しく反するようなものであってはならない。
Richter氏は主にプログラマ中心、人間中心の観点から例外をアプローチしました。Meyer氏は主に論理中心、機械中心の観点から例外をアプローチしました。そのどちらもが大切です。プログラマがプログラミングを行う限り、プログラマの期待を大きく損なう方法は肯定できません。プログラミングの生産性向上を目指す限り、機械的、形式的アプローチの模索は諦めるべきではありません。
例外安全
例外安全も契約の観点からいくつかの対応が見られます。
基本保証
基本保証は個々のデータがおかしな状態にならないことの保証です。そしてこれは「不変条件を満たす」ことと同じ意味です。
そのこの言い換えによって1つわかったことがあります。基本保証を満たす責任はオブジェクトの実装側にあります。ですので、処理の実装側の責任の範囲外です。つまり、全てのオブジェクトが不変条件を満たすよう適切に実装されていた場合、全ての処理は例外安全であり、そのソフトウェアは安全です。逆を言えば、1つでもオブジェクトが不変条件を満たさない場合、そのソフトウェアは安全ではありません。そのような場合ソフトウェアを安全にすることは困難か、そもそも不可能です。
強い保証
強い保証は契約との対応が見られません。何故なら、契約は処理を挟んだ実装側と呼び出し側の2者間に対する概念だからです。強い保証はより横断的な保証ですので、契約の範囲外です。
強い保証を契約で説明できない限り、契約を用いたプログラミングにおいても強い保証をプログラマが強く意識しなければなりません。基本保証だけでは例外回復が行えないのです。もし強い保証という概念さえも考えないと言うのであれば、それは例外回復の放棄にほかなりません。
no-fail保証
no-fail保証とは契約を持たない保証です。契約があれば破られる可能性があります。しかし、契約が1つも無ければ常に成功します。とは言え、不変条件まで含めて契約が1つもない処理というのは現実的ではありません。これは、契約が「バグ」と「例外」を区別せずどちらも「失敗」とみなしている点にあります。つまり、契約の考えで言えばno-fail保証とは「例外もバグもない保証」を意味します。
これは明らかに厳しすぎます。解決策としては2つです。1つは言語そのものがno-fail保証を直接サポートすること。もう1つは契約に「バグ」と「例外」の区別を付けるよう概念を拡張することです。
これらの関係を表で表してみましょう。強い保証が契約の概念で表現できないのは残念ですが、その他は綺麗に対応関係が見られます。
例外安全 | 契約による設計での解釈 |
---|---|
基本保証 | 不変条件を満たす |
強い保証 | 表現不可 |
no-fail保証 | 契約を持たない(全ての契約が常にtrue ) |
例外中立
契約を中心に置く例外モデルの最も特徴的な点は、例外が契約違反からのみ発生することです。そして、例外の回復とは、破られた契約を満たすように処理を異なる方法で再試行することです。契約の考えでは、例外は契約と表裏一体です。契約の違反が例外で、例外の回復が契約の達成だからです。
この例外と契約の密な関係はユニークな特徴として現れます。例外が契約の違反から発生する限り、プログラマが自らの手で例外を発生させる必要はありません。この特徴はさらに有益な結果を生みます。例外を制御構文として使用する事の難しさです。言うまでもなく例外機構は帯域脱出用の言語機構ではありません。もしそうなのであれば、別途goto
文を導入すべきでしょう。もし導入されていればgoto
文を使用すべきです。例外機構は明らかに帯域脱出を目的とした機構ではありません。
例外機構を帯域脱出として使用してしまった場合、傍から見るとある例外が例外を表現しているのか、帯域脱出を表現しているのかわからなくなります。わからなくなるということは、処理の呼び出し側はどちらもの可能性を考慮してコードを書かなければならないということです。これは例外機構だけに言えることではありませんが、ある機構に目的とは別の意味付けを行った場合、単純に想定しうる可能性が増えるだけですので複雑性が悪化し品質に悪影響を及ぼします。
そして、契約の考えを導入したとしてもやはり例外中立を正当化する理由はありません。むしろ、契約と例外の表裏一体な関係を破壊するだけになります。
以下は契約を中心とした例外の概念を図にまとめたものです。この図のフローが滞らない保証こそが例外中立そのものです。
対応しなかった概念
契約は例外を包括的に説明する優れた考えです。しかし、例外に関するいくつかの概念を説明できません。ここでは契約が何を説明できていないかを述べます。また、契約をどのように拡張すればどの程度の概念を説明できるようになるかについて筆者個人の考えを述べます。
信頼性問題
すでに述べたように、契約は処理の信頼性を説明できません。そして、信頼性の低い処理は契約を満たしても失敗する可能性がある時点で、契約で縛ることは不可能です。つまり、信頼性の低い処理は契約の考えの外にあります。
ここで、信頼性を理解するような契約の拡張を考えてみましょう。考えられる方向性としては、「契約違反とは別の原因による失敗」を直接導入することです。つまり、拡張した契約では、失敗とは「プログラマの過失による契約違反もしくはプログラマに過失のない契約違反」と定義することができます。
しかし、これでは失敗は呼び出し側か実装者のどちらか一方の責任であるという契約の大原則を放棄することになります。これでは処理が失敗しても誰が悪いのかがわからずじまいです。
「バグ」と「例外」の区別
信頼性問題での議論から、失敗には少なくとも2種類存在する事がわかります。プログラマに責任のある失敗と、そうでない失敗です。これに相当する考えは既に初日で定義しました。そう、「バグ」と「例外」です。
プログラマに責任のある失敗は回避可能です。何故なら、オブジェクトと処理が適切に実装され、表明された事前条件を満たすよう適切に処理を呼び出せば、プログラマが義務を果たしている限りそのような失敗は発生しないからです。これらは何らかの条件を変えない限り必ず失敗します。そう、「バグ」です。
前日の通り、Meyer氏は契約違反を「バグ」だと述べました。しかしそれは部分的にしか正しくありません。正しくは、プログラマに責任のある契約違反が「バグ」です。
一方、信頼性に起因する失敗はプログラマに責任がありません。そして、これらは処理をやり直したりパラメタを調整したりすれば解決する可能性があります。
例外回復とバグ
今までの契約システムでは「バグ」と「例外」を共に「失敗」として区別なく扱ってきました。よって、例外回復も正確に言えば「失敗回復」です。つまり、失敗回復の中には「バグ回復」が含まれます。
例えば前回例示したStack
のpop
メソッドを思い出してください。pop
メソッドの呼び出しに関する契約は全て信頼できる処理から成り立っています。よって、pop
メソッドの呼び出しにおける失敗は「バグ」です。例えば空スタックからpopしたとします。pop
メソッドの事前条件はnot empty?
でしたので、これは失敗します。であれば、例外が投げられます。だとして、一体何をすれば良いのでしょう?何も出来ません。バグはプログラミングによってのみ回復できます。コード上のフローによっては回復されません。
ここで初日に「バグ」と「例外」を「失敗」から分離し、各々を区別したことを思い出してください。両者は属する概念のレイヤーが違います。「例外」に対する考え方をそのまま「バグ」に適用しても上手く行きません。例外回復は「例外」しか救えないのです。
「バグ」の分離
以上を考えれば、契約の考えにおいて「例外」と考えられてきたものの中から「バグ」を取り除かなければならないことがわかります。その分離基準として信頼性を用います。つまり、信頼できる処理の失敗は「バグ」です。信頼できない処理の失敗は「例外」です。
この考えは契約に対する信頼性の導入や「バグ」と「例外」の分離等をもたらす反面、契約と例外の関係を破壊します。契約によって発生するものは「例外」ではなく実は「バグ」だったのです。
さらに言えば、信頼出来ない処理に対する契約の取り扱いをどうするかという問題が残ります。どうせ契約を満たしても信頼出来ないのですから失敗する可能性は残ります。しかし、事前条件を違反するような呼び方をした場合、これは信頼性に関せず「バグ」だと考えられます。何故なら、呼び出し側は適切に呼び出す義務があるからです。その結果処理が成功しようが失敗しようが、義務の不履行に違いはありません。
しかし、適切な呼び出し義務は処理の適切な実行という権利に対するものです。義務を果たしたのに権利が得られないのであれば契約の考えとして妥当ではないと考えられるかもしれません。
また、信頼できない処理に対する契約が有効に働く場合として、例外回復があります。例外回復はその処理の契約、もっと言うと事後条件を満たすような処理のやり直しでした。であれば、やはり信頼出来ない処理にも契約は有効であり必要であると考えられます。
以上を考えると、以下の様なシステムであれば契約のコンセプトを部分的に引き継いだまま信頼性を導入し、「失敗」から「バグ」を分離できます。
信頼性 | 失敗の種類 | 失敗の原因 | 回復 |
---|---|---|---|
高い | バグ | 契約違反 | プログラミングのやり直し |
低い | 例外 | 不明(契約違反又は信頼性に起因) | 例外回復(信頼性に起因するもののみ対象) |
契約とno-fail保証
ここでもう一度no-fail保証について考えてみましょう。拡張前の契約ではno-fail保証とは処理に「失敗」がないことの保証でした。我々はたった今「失敗」を「バグ」と「例外」の分離することができました。よって、no-fail保証とは「例外」のない保証だと考えることができます。
この考えに則れば、信頼性の高い処理は全てno-fail保証が満たされるべき、ということになります。信頼性の高い処理におけるfailとは「バグ」だからです。そして、信頼性の低い処理はいかなる手段を持ってしてもno-fail保証を満たすことができません。満たせないからこそ、信頼性が低いのです。
美しさの崩壊
これで信頼性を加味した契約を考えることができます。しかし、この拡張された契約には以前のような契約と例外の美しい対応関係はありません。拡張された契約では責任のない失敗を認めます。さらに、失敗と契約はほとんど無関係のものになります。適切に契約を満たしても処理は失敗するかもしれません。そうなればこの章で最初に述べた「例外的状況」の定義を、この拡張した契約に持ってくることができません。
筆者には契約の美しさを維持したまま信頼性を導入する良い考えが思いつきませんでした。しかし、この記事で考えた「バグ」と「例外」の区別は有用な考えです。実のところ、この考えはあるプログラミング言語の例外処理を参考に考えたものです。19日目で紹介できることと思います。
参考文献
[1]: 『クヌース先生のドキュメント纂法』 Donald E. Knuth, Tracy Larrabee, Paul M. Roberts, 共立出版 1989
[2]: 『オブジェクト指向入門 第2版 原則・コンセプト』 Bertrand Meyre, 翔泳社 2007