例外 Advent Calendar 2014の継続について
参加者が集まらなかったという経緯から独りAdvent Calendarとして始めた「例外 Advent Calendar 2014」ですが、諸事情により継続が困難となったため私Kokudoriの6日以降の投稿はありません。変に注目だけを集める形になってしまい申し訳ありません。
以下、諸事情というか、言い訳。
『契約による設計から見た例外』という記事にて述べた「契約」に対する私の理解が根本的に間違っていました。
そこから芋づる式に例外に関する私自身の考えが間違っていた、あるいは理解が浅かったことに気づきました。このような理解力では例外について私見を述べることさえ不可能となり、結果頓挫という形になりました。
考えうる限り最低で残念な結果になってしまいました。本当に申し訳ございませんでした。
初めに原則を考え出して、それから例外を見つけてゆくのだ。 Umberto Eco[1]
ソフトウェアは絶えず失敗します。わかりきった、必ず起きる失敗はバグという名前で呼ばれています。バグにソフトウェアが対応すべき責任はなく、プログラマが適切にプログラミングする事でのみ解決します。しかし、ある失敗はバグではなく、ソフトウェアが何らかの対処を施す事が要求されます。例えば、指定されたパスにファイルが無かった場合、大抵の場合これはバグとは呼ばれません。
失敗が発生した場合、「報告」「連絡」「相談」が要求され、場合によっては自力で何とかする必要があります。どこかで聞いた話みたいですね。これはおおよそすべてのドメインによって要求されるため、多くのプログラミング言語では失敗のための機構が用意されています。
バグでない失敗は、成功したり失敗したりします。例えば指定したファイルパスからファイルを取ってくる場合、ファイルがそのパスにあれば成功し、無ければ失敗します。このような失敗時の状況(例外的状況)で発生する失敗(例外)に対するプログラミング上の取り組みを例外処理と呼びます。
例外的状況
先程も述べたように、必ず起きる失敗はバグであり、例外とは別のレイヤーに属しています。であれば、例外的状況とはときおり起きる失敗の事を指します。例外機構やエラー値といったシステムは例外的状況をいかに対処するかといった道具でしかありません。使用者であるプログラマには例外的状況を正しく認識することが期待されています。
定義
Jeffrey Richter氏は「例外」を以下のように定義付けています。
例外とは、あるメンバーが、その名前から期待される処理を完了できなかった時に起こります。 [2]
これは「例外」の、より厳密に言えば「例外的状況下での失敗」の定義です。
また、これはC#を前提とした言葉であり、実際にはもう少し抽象化し「ある命名された処理が、その名前から期待される処理を完了できなかった時」を例外的状況と考える事が出来ます。
Richter氏は例として以下のC#コードを挙げています。
internal sealed class Account {
public static void Transfer(Account from, Account to, Decimal amount) {
from -= amount;
to += amount;
}
}
ここで、処理の名前はTransfer
、お金の譲渡ですから、from
から差し引かれた金額がto
へ渡る必要があります。つまり、それ以外の場合は例外的状況と言えます。このコードは一見すると上手く動きそうですが、from
やto
がnull
の場合やamount
が負の数や、Decimal
が保証する有効桁数を超える場合は上記の要求を満たせません。
また、Richter氏はamount
が0
の場合を例外的状況であると断言しています。これは、譲渡という言葉を考えた場合、一方の口座に金額が増え、もう一方から同額が減ることが明らかであると考えているためです。
譲渡に0円を含めるかどうかは要求に依存します。大抵の場合はamount
が0
の場合を例外的な状況と言っても何の問題も無いでしょう。しかし、クライアントが良しとすればamount
は0
であることを許されます。
対応
例外的状況が定義できました。お次は例外的状況への対処、例外処理です。先ほどのコードを例にすると、ある条件では成功し、別のある条件では失敗します。この時、失敗に対応できなければ、このアプリケーションは仕様を満足に満たすことはできないでしょう。
例外処理には大きく2種類あります。1つは失敗の回復であり、最も望ましい対応です。もう1つは失敗の報告であり、主に失敗を回復できない時に取られる処置です。
では具体的にどのようにして例外に対処すればよいのでしょうか。ほとんどのプログラミング言語はアプリケーションの失敗を前提としているため、何らかの例外処理機構が用意されています。例外処理の具体的な方法はその言語やライブラリで用意された例外処理機構に大きく影響を受けます。
例外 Advent Calendarでは例外そのものを考えると同時に、言語やライブラリで提供されている具体的な例外処理機構にも触れ、例外的状況にどのように立ち向かうべきかについて述べていきます。
用語
曖昧性を最小限に留めるために、例外 Advent Calendar上で使用する用語について簡単にまとめておきます。また、各用語の関係を簡潔に図でまとめてみました。
用語 | 意味 |
---|---|
成功 | ある命名された処理が、その名前から期待される処理を完了できた |
失敗 | ある命名された処理が、その名前から期待される処理を完了できなかった |
例外的状況 | ときおり失敗する状況 |
バグ | 必ず発生する失敗 |
例外 | 例外的状況から発生する失敗 |
エラー | 例外 Advent Calendarでは使用しない |
例外処理 | 例外的な状況に対応する行為やそのための機構 |
例外機構 | いわゆるなtry/catchによる例外処理システム |
エラー値 | エラー値を用いた例外処理システム |
ロードマップ
「失敗」という現実世界のモンスターと戦う上では、道具は多いに越したことはありません。我々は「失敗」を「バグ」と「例外」に2分し、「例外的状況」に注目することによってようやくモンスターの輪郭を見ることが出来ました。2日目以降ではこのモンスターを倒す可能性を秘めた心強い味方を紹介していきます。
例外安全と例外中立
Dave Abrahams氏とHurb Sutter氏の提唱した「例外安全」と「例外中立」は例外という大海における無くてはならない羅針盤です。ある例外が正しく送出されているかという例外安全と、ある例外が正しく補足されているかという例外中立を共に満たすことは安全なプログラムを書く上で無くてはならない重要な指標です。
今後、例外についての議論を行う際、例外安全と例外中立という言葉が通じると議論は円滑に進むでしょう。2日目ではこの2つの概念について詳細に述べ、例外安全と例外中立を満たすことは安全なプログラムを書く上での必要条件である事を示します。
契約による設計から見た例外
Bertrand Meyer氏は「契約による設計」という概念から例外の必要性を示すことに成功しました。契約による設計による例外へのアプローチは、今までの例外機構をより形式的に説明してくれます。Meyer氏は例外的状況をそのプログラム内の個々の部品(例えば関数だとか)自身が保持することで、自律的に例外を扱えるようになると考えました。さらに、その体系をオブジェクト指向プログラミングと組み合わせることでより各々の独立性が上がり、保守性が高まる旨を主張しています。
オブジェクト指向については直接関係しない話なのでここでは述べませんが、3日目では契約による設計についてより詳しく考えます。
例外大統一理論
例外安全と例外中立、そして契約による設計はそれぞれ別々の分野から端を発した概念です。しかし、この2つの考え方は同じ事を別の視点から述べている似すぎません。
4日目では2つの概念の統合を図り、それぞれが相補的な関係にあることを述べます。また、ここで述べた議論は次の検査例外とも深く関係しています。
検査例外再考
検査例外はJavaで実装された非常にユニークな例外機構の事です。検査例外は現在でも賛否両論があり、多くの議論がなされてきました。しかし、その議論の多くはJavaの検査例外固有の問題と、検査例外一般の問題を区別しないものでした。
5日目では検査例外の非形式的な説明を述べ、Java固有の部分とより一般化された検査例外を区別します。その上で、検査例外に対する批判を考えます。最後に、例外安全や例外中立、契約による設計と検査例外がどのような関係にあるかをまとめます。
エラー値
よく例外機構と引き合いに出される考えとしてエラー値があります。エラー値は今でも使われていますが、例外機構のない言語での使用等、限定された使われ方をしています。
6日目ではエラー値の問題点、特に例外機構と比べた上での問題点を考えます。
例外機構
いわゆるtry/catch形式の例外機構は例外的状況に相対する最も一般的な道具として知られています。しかし、例外機構とて万能ではありません。例外機構が解決した問題も多くありますが、例外機構が持ち込んだ問題もまた多く存在してしまっています。
7日目では初心に帰り、例外機構について考えます。特に、例外機構が解決した問題よりも例外機構が持ち込んだ問題について焦点を当てます。
Goの多値
Go言語はその古典的な言語仕様から度々批判の対象となっています。Goには例外機構が存在せず、エラー値を発展させたような、言語から特別扱いを受けた多値が存在するのみです。2009年に登場した言語が例外機構を有していない事はとてもユニークであり、一考の価値があります。
8日目ではGoの多値がどのように古典的なエラー値から発展し、過去の問題を解決したのか。そして、例外機構を持たない理由を探るため、例外機構が持ち込んだ問題をGoの多値は解決しているのかどうかについて考えいます。
Maybe/Either
例外機構に対するもう1つの選択肢としてMaybe/Eitherがあります。これらは構造的データ型というML由来のデータ構造を用いたもので、例外機構による例外ハンドリングと比べてやり方を大きく転換させています。
9日目ではエラー値や例外機構、Goの多値と比較した時のMaybe/Either方式のメリット・デメリットを述べます。さらに、Maybe/Either方式での限界性についても考えます。
finally
例外機構とよくセットで導入される機構としてfinallyがあります。これは例外の発生や補足とは無関係に実行される処理を登録するもので、主にリソースの開放などに用いられます。
10日目ではそもそも例外の発生や補足に無関係な処理というものを例外の観点からどう考えれば良いのかについて考えます。そして、例外機構同様finallyもプログラミング言語によって様々な発展が見られます。ここではD言語のscopeとGo言語のdeferを紹介し、一般的なfinallyと対比させた上でメリット・デメリットを述べます。最後に、RAIIについても簡単にまとめます。
nullはなぜ悪いのか
nullの生みの親とされるTony Hoareによるnull批判、いわゆる「nullは10億ドルの過ち」[3]発言からnullに対する批判が次々に行われています。nullの問題は例外の枠組みからも考えることが可能なので、ケーススタディとして扱ってみましょう。
11日目ではnullの何が問題とされているかについてまとめます。そして、例外の文脈からnull問題についてアプローチします。null参照による例外発生は例外安全を満たす上でどのような意味があるでしょう。契約による設計を実践する上でどのような障害となるでしょう。
例外の粒度
今までは例外的状況やその処理機構についてを主として取り扱ってきました。しかし、例外そのものの粒度についてはどうでしょう。例外を正しく表現するには、発生しうる例外を全て新しい例外として考える事が考えられます。しかし、それだと似たような例外を統一的に扱えず、抽象度が低く再利用や保守の難しい物になるでしょう。これは例外だけに限った話ではなく、モデル化についての一般的な問題です。
12日目では例外の粒度について考えた上で、各々の例外処理における例外設計の特別な注意点について述べます。
要件と例外
例外を要件の種類から考えるアプローチがあります。最も有名なものとして、 Andrew Hunt氏とDave Thomas氏による「すべての例外ハンドラーを除去してもプログラムが動作できなければ、例外が不適切に使用されている(筆者注:文献の内容を元に筆者による要約)」[4]という指標があります。これは「例外処理を機能要件から分離すべきである」と言い換えることができます。
13日目では要件の種類から例外を考え、各々の要件に適した例外処理についてまとめます。
非同期と例外
21世紀のプログラミングと言えば並列と平行です。これらに共通して言える事として、非同期の容易な扱いが要求されるという問題があります。現在の多くのプログラミングモデルは同期を前提としており、例外機構も同様です。
14日目では既存の言語の非同期に対する例外の取り組みについていくつか俯瞰し、例外機構がいかに非同期と相性が悪いかを述べます。その上で、非同期モデルに適した例外について考えます。
純粋と例外
プログラムは純粋性という概念から「純粋である」ものと「不純である」ものの大きく2つに分類することができます。そして、この区分は例外と深く関係しています。
15日目では純粋なプログラムでの例外モデルが不純なプログラムではそのまま適用できない例を挙げます。特に純粋性と契約による設計との関係は深く、不純なプログラムではいくつか考慮すべき問題が増えてしまいます。
継承と例外
例外機構と継承はほとんど同時期に多くのプログラミング言語を媒介に普及していきました。これは必然だったのでしょうか。それとも、ただの歴史的な偶然だったのでしょうか。
16日目では継承が例外処理にどのような影響を与えるかについて考えます。そして、継承(や部分型)のないプログラミング言語における例外処理とどのような違いが生まれるのかについて考察します。
アスペクト指向と例外
アスペクト指向プログラミングはオブジェクト指向プログラミングを発展させたプログラミングモデルの1つで、モノの定義とは別に横断的な関心事という軸を新たに与え、定義的な軸と関心的な軸による平面的なプログラミングモデルです。
アスペクト指向プログラミングでは例としてロギング等がよく挙げられますが、17日目では例外処理という関心事を横断的に扱えるのかどうかについて考察します。
C#の例外
ケーススタディとしてC#の例外に対する取り組みを総合的に判断してみましょう。
18日目ではC#の例外処理機構、例外に対するC#特有の問題、C#の取り組みに対する問題と課題について述べます。
Haskellの例外
ケーススタディ2弾目はHaskellです。
19日目ではHaskellにおける例外への取り組みを総合的に判断します。Haskellはいくつかの点でユニークな取り組みを行っています。例えば、Haskellは純粋な世界では例外機構を廃止しMaybe/Eitherを導入し、不純な世界では例外機構を導入しています。例外処理をある区分によって使い分けている意味で、Haskellの例外処理は先進的です。
Erlangの例外
ケーススタディ3弾目はErlangです。
20日目ではErlangの例外処理について考えます。Erlangは安定的な分散プログラミングを行うために、例外におけるいくつかのユニークな取り組みが見られます。また、Erlangでのプログラミングモデルそのものが特異です。例えば、Eralngでは失敗を例外処理ではなく通知とリトライによって回復する事があります。
Ruby on Railsの場合
ケーススタディ4弾目はRuby on Railsです。今まではプログラミング言語をケーススタディとして見てきましたが、ライブラリやフレームワークがどのように例外を扱っているかを考えることは言うまでのなく重要です。
21日目ではRuby on Railsが補足しうる例外と送出しうる例外の2軸からRuby on Railsにおける例外への取り組みを総合的に評価します。
.NET BCLの場合
ケーススタディ5弾目は.NET BCLです。BCLとはBase Class Libraryであり、いわゆる標準ライブラリと同等のものです。
22日目では.NET BCLの例外の取り組みについて総合的に評価します。.NET BCLはとても大きいので、.NET BCLを通して巨大プロジェクトにおける例外への取り組みを考えることができます。
AngularJSの場合
ケーススタディ6弾目はAngularJSです。
23日目ではAngularJSの例外への取り組みについて考えます。AngularJSはJavaScript上のフレームワークですので、非同期プログラミング上での例外への取り組みを考えることができます。
例外の歴史
今まで見てきた例外への取り組みについて、プログラミング言語の観点から簡単に歴史として俯瞰しようと思います。よく、「例外機構はエラー値の次世代的な機構として誕生した」といった説明を目にすることがあります。しかし、エラー値の代表格であるC言語よりも前に例外機構を持つ言語は存在しています。1964年のPL/Iは現代的な例外機構の走りと見ることができます。実に50年も前に例外機構は存在していたのです。
24日目では浅ましいまでに簡単に例外にまつわる歴史を俯瞰し、進化の方向性について考察します。過去を見、現在を知ることで、未来の方向が見えてくるかもしれません。
未来の例外
最後に、未来の例外について筆者個人の稚拙な考察を述べようと思います。
例外の歴史を俯瞰するに、現代的な例外機構は1964年のPL/Iまで遡ることが出来ます。が、裏を返せば現状の例外機構はちょうど50年間ほとんど進化を遂げていない事も意味しています。しかし、HaksellのMaybe/EitherやGoの多値など最近ようやく例外周りにも活発な動きが見られるようになりました。例外は決して枯れた技術ではありません。今なお模索し続けている生きた技術なのです!
参考文献
[1] 『薔薇の名前』 Umberto Eco, 東京創元社 1990
[2] 『プログラミング.NET Framework 第4版』 Jeffrey Richter, 日経BP社 2013
[3] 『Null References: The Billion Dollar Mistake』 http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare
[4] 『達人プログラマー―システム開発の職人から名匠への道』 Andrew Hunt, Dave Thomas, ピアソンエデュケーション 2000