困った例外さん

ついらーで例外についてちょっと長いやりとりがありました。長いので、気になる人だけ続きをどうぞ。やりとりが長いだけで内容自体は大したことないです。


https://twitter.com/lyrical_logical/status/258803533174943744

lyrical_logical: GC が例外安全性の何を解決してくれるんだろう?メモリリークくらいだよなあ。例外安全性は結局のところ、例外という機能により増える実行パスに対する事後条件、不変条件をきちんと考えよう、という話になると思ってるので、まずはそっちからだよなあと思うのであった

https://twitter.com/k_satoda/status/258966409936244736

k_satoda: @lyrical_logical 「例外という機能によって増える実行パス」は正しくは「エラーによって増える〜」であって、エラー通知方法に例えば戻り値を使っても変わらないのでは?これを例外のせいにするような言説を例外を変に避ける理由としてよく見るような気がしてるので、念のため。

https://twitter.com/lyrical_logical/status/258972369501167616

lyrical_logical: @k_satoda 実行パスの総数についていったわけではなくて、例外によって増えるパスの道筋とでもいえばよいのか、について言ったつもりでした

https://twitter.com/k_satoda/status/258973774777577472

k_satoda: @lyrical_logical んー?やっぱり「例外によって増えるパス」というのが何かの間違いのような気がします。処理しなければならないエラー発生の可能性が同じだとして、エラー通知方法に例外を使った場合のみ存在し、戻り値を使えば存在しなくなるような実行パスがあるのでしょうか?

https://twitter.com/lyrical_logical/status/258974292337909760

lyrical_logical: @k_satoda 実行パスの定義に相違がある気がしますね。ボクはコードの上で話をしているつもりなのですが

https://twitter.com/k_satoda/status/258977061849411584

k_satoda: @lyrical_logical ソースコードをなぞる形でパスを数え上げれば戻り値(で分岐)を使ったほうが数が増えるでしょうし、ソースコードではなく、動作の分岐を列挙していった場合のパスの数であれば、同じ仕様のプログラムに対して例外を使おうが戻り値を使おうが同じでしょうし・・・。

https://twitter.com/lyrical_logical/status/258977897082126336

lyricall_logical: @k_satoda あー誤解されてる点がわかった気がします。増えるじゃなくて、生じるといえば良かったのかな。

https://twitter.com/k_satoda/status/258980705760071680

k_satoda: @lyrical_logical うーん・・・。すいません。「増える」でも「生じる」でも同じようにしか思えません。 ・・・あまり興味の無いところを僕が一方的にほじくってしまっているようであれば、スルーされるのがよいかもしれません。

正直面倒ですが一応書いておきます。


k_satoda さんの仰るとおり、扱わなければならないエラーの総数が、例外を使うかどうかで変わったりは勿論しません。しかし、例外を使うと面倒な実行パスが生じがちです。
例えば Java で以下のようなメソッドがあったとしましょう。

public static void copyStream(InputStream in, OutputStream out) throws IOException {
  byte[] buffer = new byte[1024];
  int size;
  while ((size = in.read(buffer)) != -1)
    out.write(buffer, 0, size);
}

上記のメソッドにはどのような問題が潜んでいるでしょうか?簡単ですね、InputStream#read によって生じた例外なのか、OutputStream#write によって生じた例外なのか、呼び出し側からは区別がつかなくなってしまいます。Throwable#getMessage や Throwabel#getStackTrace とか使って頑張れば一応区別つくとは思いますが…
つまり「InputStream#read -> copyStream の外」という実行パスと「OutputStream#read -> copyStream の外」という二つの実行パスがあるわけですが、copyStream の外からはそれらが区別できないわけです。
じゃあ関数内でハンドルして適切に例外を投げ直せばいいじゃないかと思ったあなた。実際にやってみてください。どれだけ煩雑になることか。
勿論現実には、どちらで失敗したかを知る必要がないことも多いです。また、例外を使わずに返値にエラー情報を含める場合にも、エラーをまともに区別せずに、兎に角 -1 だ!とかやってると、同じことです。


あと C++ の場合。例を挙げる気力体力がわかないので簡単に説明しますが、C++ のスコープは Java や他の言語のそれとは違い、抜けるときにスコープに束縛されている値のデストラクタが呼び出されるます。例外が投げられると、大体スコープの外、何ならスタックフレームも飛び越えて一気にジャンプしてしまうわけですが、そのときに一体どれだけのデストラクタが呼ばれるでしょうか。
勿論例外が投げられようと例外安全、例外中立になるよう努めてコードを書くのが C++ プログラマというものですが、これは中々難しいことです。特にテンプレートや演算子オーバーロードが絡んでくると、どこで例外が発生するのか、判別するのは物凄く難しい仕事になります。C++11 の noexcept で少しはましになる…といいなあ…というところですね。
これは例外が悪いわけではなく、C++ という言語が悪いのですが、例外という強力な言語機能が他の言語機能とうまく馴染めてないとも言えるので、例外もまあちょっとは悪いといえるんじゃないかな、と思います。


更に、関数型プログラミングをする場合。例外は邪魔で仕方がない存在になります。
関数定義内に例外もしくは例外を投げる式(より正確には、関数の定義域にない値を適用する可能性のある式)が現れた途端に、それを catch しない限りは、その関数は部分関数となります。部分関数がどう面倒なのかは…めんどくさいのでこちらなどを参照してください。非正格評価を行う Haskell 限定の話とかもあるので関数プログラミング一般の話ではないんですが…
ちなみに定義域にない値が適用されないことが自明に分かっている場合には、余計な定義を書かずに済んで便利なこともあります。
あと部分関数に対し、値が定義域に属しているかどうかを知ることができる場合には、便利に使えちゃったりもします。Scala の PartialFunction なんかがそうですね。


言語毎、プログラミングスタイル毎の詳細について話していると話が終わらなくなってしまいますから、もう極論してしまいますが、例外というのは return でなく goto なわけです。goto が特定の状況を除いて邪なのは、よく知られている通りです。「特定の状況」に当てはまるような例外は、少ないんじゃないかなあ…と思います。


以下蛇足。

逆に、例外というのは関数から複数の型の値を返すことができる、そして catch 節によって型による分岐ができる、つまり型の和とパターンマッチが実現できる!やった!とかそういう見方もありますが、これはジョークです。