これは Swift Tweets の発表をまとめたものです(次回開催はこちら)。イベントのスポンサーとして Qiita に許可をいただいた上で投稿しています。
第 1 部: Swift の 4 種類のエラーについて
あまり知られてませんが、エラー処理について、 Swift 2.0 設計時に Core Team がまとめた "Error Handling Rationale and Proposal" というドキュメントがあります。このドキュメントは、僕が去年 try! Swift で発表した際にも参考文献にしました。長いし(僕にとっては)英語が難しいし、具体例も少ないしで読むのがとても大変でした。
その中でエラーの分類について記述があるんですが、当時ピンと来なかったのが 1 年かけて逆に素晴らしいと思えてきたので、今日はそれについて発表します。本質的で重要なことだと思うので、もし1年前に戻れるなら try! Swift の発表テーマをこれに変更したいくらいです。
エラーの種類
それによると、エラー発生時プログラマに『どのように対応させたいか』によって、エラーは
- Simple domain error
- Recoverable error
- Universal error
- Logic failure
の4種類に分類されます。
『どのように対応させたいか』というところがキモです。 エラーの分類というと、エラーの内容によって分類してしまいそうですが、そうではなくエラーに『どのように対応させたいか』です。 どういうことでしょうか。
例
例として、文字列を整数に変換する関数 toInt
を考えてみます。この関数は、 "123"
のような文字列であれば整数に変換可能ですが、もし "xyz"
のような文字列が渡された場合は整数に変換することができず、エラーとなります。
Simple domain error
文字列→整数の変換に失敗するなんて原因は明白なんだから、エラーが起こったかどうかさえわかればいいんだ、と考える場合に採用します。エラーが発生したことだけを伝え、その原因についての情報は伝えません。
Swift なら、 Optional
を使って戻り値の型を Int?
とし、 nil
を返すことで Simple domain error を表現できます。
func toInt(_ string: String) -> Int? { ... }
guard let number = toInt(string) else {
print("整数を入力して下さい。")
return
}
// `number` を使う処理
Recoverable error
たとえ文字列→整数の変換であっても、原因によって処理の方法が異なるだろう、と考える場合に採用します。たとえば、文字列が整数だったけど Int
の範囲で表せない(オーバーフローする)場合とその他の場合を区別したいかもしれません。
Swift なら、 Error
型で Recoverable error を表現できます。 toInt
には throws
を付与し、 do
- try
- catch
を使ってエラー処理をします。
func toInt(_ string: String) throws -> String { ... }
do {
let number = try toInt(string)
// `number` を使う処理
} catch ConversionError.overflow {
print("\(Int.min)〜\(Int.max)の値を入力して下さい。")
return
} catch {
print("整数を入力して下さい。")
return
}
Recoverable (回復可能)というのは、残りの Universal error と Logic failure が回復可能 でない こととの対比です。 Simple domain error も回復可能なので Recoverable error という名前は微妙な気もします。
Universal error
整数に変換できないような不正な文字列が toInt
に渡されたらプログラムが停止すればいい、エラー処理をする必要はないと、考える場合に採用します。
Swift では、 Universal error を発生させるために fatalError
を使います。 Swift 3 時点では、 fatalError
を処理して回復することはできません。
func toInt(_ string: String) -> Int {
guard isPossibleToConvert(string) else {
fatalError()
}
...
}
let number = toInt(string) // 変換に失敗したらクラッシュ
// `number` を使う処理
Logic failure
整数に変換できない文字列を toInt
に渡していること自体がバグだ、コードを修正する必要がある、と考える場合に採用します。 Logic failure は実行時に起こってはいけないので、起こった場合の挙動は未定義です。
Swift では、 Logic failure を表すために precondition
を使います。 precondition
は -Ounchecked
でビルドすると無視され、実行時のオーバーヘッドをなくすことができます。
func toInt(_ string: String) -> Int {
predondition(isPossibleToConvert(string))
...
}
let number = toInt(string) // 変換に失敗したら未定義動作
// `number` を使う処理
『どのようにエラーに対応させたいか』によってエラーを分類する
このように、文字列→整数の変換エラーという内容ではなく、『どのようにエラーに対応させたいか』によってエラーを分類します。どうしてエラーの内容ではなく『どのようにエラーに対応させたいか』によってエラーを分類するのでしょうか。
当たり前ですが、僕らが関数やメソッドを設計するときには、利用者に『どのようにエラーに対応させたいか』を考えなければなりません。 nil
を返すのか、 Error
を throw
するのか、それともクラッシュさせるのか、必ず判断をしているはずです。そう考えると、『どのようにエラーに対応させたいか』によるエラー分類は特別なことではなく、関数・メソッドの設計時には誰もが当たり前にやっていることのはずです。
エラーの使い分け
しかし、僕らは一貫した適切なポリシーを持ってエラーを分類できているでしょうか。もしそうでなければ、アプリやライブラリ全体としてポリシーのブレたわかりづらい設計になってしまっているはずです。
例
"Error Handling Rationale and Proposal" には、4種類のエラーの使い分けの例も書かれています。それを紹介しましょう。
Simple domain error の例
整数→文字列の変換エラーが挙げられています。発生の前提条件が明確なので、エラーが起こったという結果さえわかれば良いという考えです。
guard let number = Int(string) else {
print("整数を入力して下さい。")
return
}
// `number` を使う処理
Recoverable error の例
ファイル入出力やネットワークのエラーが挙げられています。発生条件が明確でなく、どのように回復すべきか原因によって対応方法が異なるので、その手段を提供するべきという考えです。
// ※説明のために架空の `Data` の `init` を使っています。
do {
let data = try Data(at: path)
// `data` を使う処理
} catch FileError.noSuchFile {
// ファイルがない場合のエラー処理
} catch FileError.permissionDenied {
// パーミッションがない場合のエラー処理
} catch {
// その他の場合のエラー処理
}
Universal error の例
メモリ不足やスタックオーバーフローなどのエラーが挙げられています。利用者側で対応しようがないのでプログラムを停止すべきという考えです。
func reduce<T, R>(_ range: CountableRange<T>, _ initial: R, _ combine: (R, T) -> R) -> R {
guard let first = range.first else { return initial }
return reduce(
range.startIndex.advanced(by: 1) ..< range.endIndex,
combine(initial, first),
combine
)
}
reduce(0..<1000000, 0) { $0 + $1 } // スタックオーバーフローでクラッシュ
Logic failure の例
Array
のインデックスがはみ出てしまった場合や、 nil
が入っている Optional
に対して Forced unwrapping を実行してしまった場合などが挙げられています。
let array = [2, 3, 5]
print(array[3])
使い分けの考え方
注意してほしいのは、あくまでこの例は「多くの場合」に適しているだけで、絶対的な指針ではありません。繰り返しになりますが、同じエラーでも『どのようにエラーに対応させたいか』次第で、 4 種類のどれを採用することもあり得ます。
たとえば、 Universal error の例としてメモリ不足が挙げられていますが、巨大なメモリ領域を確保するようなケースでメモリが足りなければエラー処理をしたいでしょう。そのようなケースでは Simple domain error が適切です。
僕が一番おもしろいと思い、感銘を受けたのが Logic failure です。どうして Array
はインデックスが範囲外だった場合に Simple domain error としてnilを返さないのでしょうか。 Dictionary
はキーに対応した値がなければ nil
を返します。
僕が初めて Swift に触れたとき、これはパフォーマンス上の理由だと考えていました。インデックスが不正でないかチェックしようとするとその度にオーバーヘッドが発生します。画像処理などで繰り返し subscript
が呼ばれるときに、そのオーバーヘッドは無視できません。
もちろんそういう理由もあるでしょう。しかしよく考えてみると、 Array
の要素に subscript
でアクセスするのにインデックスがはみ出してしまうようなケースは、ほとんどがコーディング時のミスが原因だとわかります。
試しに、インデックスが範囲外だった場合にエラー処理をして回復する必要がある具体的な処理を何か思い浮かべてみて下さい。僕が思い付いたのは番兵の代わりや畳み込み等の画像処理くらいです。コードに問題があるのであれば回復『不能』であり、実行時にエラーに対処することはできません。コードのバグを修正すべきであり、 Array
の subscript
のエラーを Logic failure として扱うのは理にかなっています。
ところで、 Array
のインデックスがはみ出たときはクラッシュ、つまり Universal error なんじゃないかと思うかもしれません。次のコードを実行してみて下さい。
let array = [2, 3, 5]
print(array[3])
クラッシュしましたね。では、ファイル名を out-of-bounds.swift として、次のように -Ounchecked
で実行してみて下さい。実行する度に結果が変わる(未定義動作な)のが確認できるはずです。
swift -Ounchecked out-of-bounds.swift
Array
のインデックスが範囲外になるのはコードの問題( Logic failure )なのだから、実行時に範囲チェックする必要はないわけです。その上で、開発時( -Ounchecked
でないとき)は素早くコードの誤りに気付けるようにチェックして停止させてくれるわけです。
まとめ
- 『どのように対応させたいか』でエラー分類
- 前提条件が明確→ Simple domain error
- 原因ごとに対応→ Recoverable error
- 回復不能→ Universal error
- コードの誤り→ Logic failure
"Error Handling Rationale and Proposal"のエラー分類はよく考えれば当たり前のことです。しかし、改めて考え直すと、自分が設計した関数やメソッドでこれを徹底できていたと自信を持って言える人は少ないのではないでしょうか。
第 2 部: Java との比較からエラー分類の重要性を探る
ところで、僕が大学生だった頃、ヒューマンインターフェース工学の授業で人間の特性 8 箇条というものを習いました。「人間は気まぐれである」、「人間はなまけものである」、「人間は不注意である」、「人間は根気がない」、「人間は単調をきらう」などです。これは、何かを設計するときには人間はそのような欠点を持っているものと考えて、それでも問題が起こらないようにすべきだという指針です。
C 言語とエラー処理
しかし、当時僕が使っていた C 言語はとてもそのように設計されているとは思えませんでした。
たとえば、 C 言語ではファイルを開くには次のようなコードを書きます。もし「不注意」でエラー処理を忘れてしまったら、 file
を使おうとしてクラッシュするまで気付くことができません。
FILE *file = fopen("filename", "r");
if (file == NULL) { // ファイルを開くのに失敗したとき
// エラー処理
}
// `file` を使う処理
人間は「気まぐれ」でエラー処理を書いたり書かなかったりします。異常系は起こらないことが多いので「なまけ」て省略してしまいがちです。省略しないと決めても「不注意」で簡単に忘れてしまいますし「根気がない」ので長続きしません。そして、エラー処理の大半は「単調な」作業です。
Java と検査例外と Swift
初めてJavaに触れて検査例外を知ったとき、素晴らしい仕組みだと思いました。エラー処理を忘れたらコンパイラが教えてくれます。ヒューマンエラーを防ぐ画期的な仕組みです。
try { // try-catch を書かないとコンパイルエラー
RandomAccessFile file = new RandomAccessFile(new File("filename"), "r");
// `file` を使う処理
} catch (FileNotFoundException e) { // ファイルを開くのに失敗したとき
// エラー処理
}
しかし、 Java の検査例外は失敗とみなされ、 C# や Kotlin などの後続言語には取り入れられませんでした。ヒューマンエラーを防ぐ素晴らしい仕組みだったのになぜでしょうか。
Java の検査例外が良くないと言う場合には、 Java の検査例外の構文が良くないのか、検査例外という概念そのものが良くないのかを区別して考えなければいけません。僕の考えは前者です。現に、 Swift は検査例外に似たエラー処理機構を取り入れ、うまく機能しています。
検査例外には二つの性質があります。一つはエラーの種類を静的型で扱うこと、もう一つはエラー処理を強制することです。 Swift でサポートされているのは後者で、前者については議論が続いています。なので、 Swift で検査例外(的なもの)がうまく機能していると言っても、検査例外の『一部』がうまくいっているだけかもしれません。
しかし、僕は Swift で検査例外(的なもの)が機能している理由は静的型エラーをサポートしないからではない思います。 Swift で検査例外(的なもの)がうまくいっているのは、構文の改善によるところが大きいでしょう。
defer
によって finally
の煩わしさに対処し、ラムダ式・クロージャ式との相性の悪さを rethrows
が解消しました。また、関数に throws
が付与されている場合、呼び出し箇所に try
の記述を強制することで Implicit control flow problem を軽減しています。そして何より、 try!
の存在が大きいです。
静的なチェックは万能ではありません。たとえば、文字列→整数の変換は失敗する可能性がある処理ですが、 UI 側で入力できる文字が数字に限定されているかもしれません。そのような文字列は必ず整数に変換できますが、コンパイラはそこまで理解できません。そんなときに、 Simple domain error や Recoverable error を Logic failure として扱う簡単な構文が必要です。 !
や try!
がそれに当たります。これがないと、検査例外は時に不要なエラー処理を強制する煩わしいものと化してしまいます。
例えば、もし Java で try!
と同じことをしようとすると次のようになります( FormatException
を throw
し得る toInt
というメソッドがあるとします)。
int number;
try {
number = toInt(string);
} catch (FormatException e) {
throw new RuntimeException("Never reaches here.");
}
Swift なら次の通りです。シンプルですね!
let number = try! toInt(string)
絶対に起こらないエラーを Logic failure 化するために、毎回 Java のようなコードが必要なら検査例外にうんざりすることでしょう。
検査例外とエラーの分類
しかし、今日の主題はエラーの分類です。 "Error Handling Rationale and Proposal" のエラー分類を理解するにつれ、僕は Java とのエラー分類の違いが、 Swift で検査例外が機能する理由の一つではないかという仮説を持つようになりました。
Java で throw
できるものはすべて Throwable
のサブタイプです。 Throwable
のサブタイプには大きく分けて
Exception
RuntimeException
Error
の三つがあります。
Java 公式チュートリアルや必読書と言われる Effective Java によると
- 回復可能なエラー→
Exception
- プログラミングエラー→
RuntimeException
- 実行継続不能な致命的エラー→
Error
という使い分けが推奨されています。
言いかえれば、
-
Recoverable error →
Exception
-
Logic failure →
RuntimeException
-
Universal error →
Error
ということです。この使い分け自体は妥当だと思います。
しかし、 Java の設計当時、 Java の設計者たちはこの分類を明確に意識できていなかったのではないかと僕は疑っています。 Java 設計者の気持ちになって考えてみましょう。
今、検査例外という素晴らしい仕組みを導入し、エラー処理を強制することを考えています。しかし、すべてのエラー処理を強制しようとすると破綻します。たとえば、配列の要素にインデックスでアクセスするときにすべて try
- catch
を強制するのは非現実的です。 Java ではそのようなときに RuntimeException
のサブクラスである ArrayIndexOutOfBoundsException
が投げられます。 RuntimeException
は Logic failure なので Swift と同じです。何が問題なのでしょう?
僕は、設計当時 RuntimeException
は Logic failure ではなく「とても全部処理していられない頻度の高いエラー」と考えられていたのではないかと疑っています。全部処理するとコードがぐちゃぐちゃになってしまうので処理するかはプログラマに任せよう、と。
その証拠と僕が考えているのが、 RuntimeException
( Logic failure )が Exception
( Recoverable error )のサブクラスになっていることです。そのため、 catch(Exception e)
と書くと Exception
( Recoverable error )だけでなく、サブクラスである RuntimeException
( Logic failure
)まで捕まえてしまいます。
しかし、 Logic failure は最も回復『不能』なエラーです。決して実行時に処理してはいけません。 Effective Java にも RuntimeException
は catch
すべきでないと書かれています。それであれば RuntimeException
を Exception
のサブクラスにしたのは明確な誤りです。このエラー分類の曖昧さが標準ライブラリの設計に影響しているように思えます。
一番ひどいのは文字列→整数の変換です。このメソッド parseInt
が投げるのは RuntimeException
のサブクラスです。つまり、文字列→整数の変換失敗はcatchして処理してはいけないということです。
しかも、他に文字列を整数に変換できるかチェックするためのメソッドが提供されているわけでもありません。つまり、ユーザーが入力した不正文字列に対して「整数を入力して下さい」と表示するには、 Logic failure を catch
して回復処理を書くしかないのです。もし、初めから RuntimeException
を Logic failure だと考えていたら、このような設計はあり得なかったでしょう。 Logic failure はコードの誤りであり、実行時に回復してはいけないのですから。
この他にも、 Java の標準ライブラリには引数が不正だった場合に RuntimeException
を throw
するメソッドがたくさんあります。何せ、 IllegalArgumentException
という RuntimeException
のサブクラスがあるくらいです。
標準ライブラリの設計に引っ張られてか、 Java の世界ではこれらを Logic failure として扱うことを推奨しています。つまり、引数に前提条件があるメソッドに対して誤った値を渡すのは、前提条件のチェックを怠ったコードのバグだからコードを修正すべきだという主張です。
しかし、本当にそれらを Logic failure として扱うべきでしょうか。 Array
のインデックスがはみ出てしまうエラーが Logic failure として妥当なのは、前提条件(はみ出ていないか)をチェックして分岐するようなコードがほぼ不要だからです。前提条件のチェックが頻繁に行われるなら、「不注意」によってチェックを忘れてしまうこともあるでしょう。せっかく「不注意」を防ぐために検査例外が導入されたのに、その「不注意」が(コンパイル時ではなく)実行時まで検出できないのでは何のための検査例外なのでしょうか。
前提条件が明確なエラーの多くは Simple domain error として扱うのが適切です。 (→追記にて訂正) 最も回復が簡単な Simple domain error を回復不能な Logic failure にしてしまったエラー分類の曖昧さこそ Java の失敗だと僕は思います。そして、「不注意」によるエラー処理忘れを防げるという検査例外の大きな恩恵の一つを Simple domain error について捨ててしまったがために、 Java の検査例外を使っていてもありがたみよりも煩わしさを感じる人が多いのではないでしょうか。
Swift は構文が優れているのはもちろんのこと、エラー分類が適切だからこそ安全かつ快適なエラー処理を実現できているのだと思います。「不注意」な僕にとっては最高の言語です!
まとめ
- 人間は「不注意」なのでエラー処理忘れをコンパイラが検出できることは素晴らしい
- 静的なチェックは万能ではないので、 Simple domain error 、Recoverable error を Logic failure 化する
!
やtry!
が重要 - Java では Simple domain error が適切と思われるケースで Logic failure が採用されており、「不注意」によるエラー処理忘れを静的に検出できる恩恵を受けづらい
- エラー分類が適切でなければ安全かつ快適なエラー処理は実現できない
追記(2017-01-18)
id:sawat IllegalArgumentException が Logic failure じゃない場面なんてそんなに多くない気がするが…。
はてブでこのようなコメントをいただいて考えていたのですが、たしかにその通りなように思います。
Logic failure かそうでないかの境目は、エラーが発生したときにそれをハンドリングして何らかの処理を実行することがあるかどうかですが( Array
の index out of bounds を処理することが滅多にないので Logic failure としたのと同じように)、 前提条件が明確な IllegalArgumentException
(引数に渡された不正な値が原因のエラー)であっても、それをハンドリングする必要があるようなオペレーションは下記のケースを除いてほとんど思い付きませんでした。
Simple domain error となるのは次のようなケースです( removeLast
メソッド等は引数が不正なわけではないですが、第一引数にオブジェクト自身を渡す関数と読みかえれば引数の不正と同じ話です)。
- 変換(例:
parseInt
, JSON のデコード等) - 値の不在(例:
pop
,Iterator
のnext
等) - 計算(例: ゼロ除算(単位ベクトルや逆行列の演算)、負の数の平方根)
- 数を引数にとる処理(例: 配列の生成)
値の変換はプログラムの外から来た値がインプットとなる場合が多いので、コードレベルで不正値を防ぐことができないケースが多く、エラーをハンドリングしたいです。コレクションに対する操作も、 DB から取得するなど外部から来たものを操作することが多く同様です。
計算や数を引数にとる処理は、オペランドや引数が計算の結果として導かれた結果、簡単に値が範囲外になってしまいます。 sqrt(a - b)
の a - b
が負であるような場合に、事前チェックが必要なのであれば、それは Logic failure ではなく Simple domain error であるべきです。コレクションのケースも同様に、他のオペレーションの結果としてコレクションを得た場合には、 pop
する前に空でないことなどのチェックが必要となります。
これらに共通なのは、引数に渡される値をコードレベルでコントロールできない(コントロールするのが難しい)ことです。そのため、引数に不正な値が渡されるケースがコードの誤りとは限らず、 Logic failure ではなく Simple domain error として扱いたいわけです。
しかし、不正引数で引き起こされる他の多くのケースはエラーは捕まえてもどうしようもないことが多いように思います( MessageDigest
の getInstance
に未知のアルゴリズムを表す文字列が渡された場合など)。なので、
前提条件が明確なエラーの多くはSimple domain errorとして扱うのが適切です。
というのは 誤り だと言えそうです。訂正します。
ただ、 Simple domain error を発生させるような処理は利用頻度が高く、それらが安全にハンドリングできないことによるストレスは大きいとは思います。 Java には Simple domain error を安全かつ快適に扱う良い方法がなく( try-catch は大がかりすぎますし、 null
を返しても null
チェックを無視できてしまうので安全でありません。しかも、 null
はネストできないので、たとえば Iterator
の next
が null
でエラーを表すと、次の値が存在しないのか、次の値が null
なのかを区別できません。)、 Simple domain error であるべきエラーが Logic failure として扱われているという点自体は間違いではないので、全体的な論旨はおかしくないと思います。
前半の話はおおよそ納得したのですが、Java の検査例外の話がいまいちしっくりきませんでした。
「Simple domain errorが適切と思われるケースでLogic failureが採用されて」いるケースというのは、NumberFormatException 以外にどんなものがあるのでしょうか。ArrayIndexOutOfBoundsException や NullPointerException は別に「Simple domain errorが適切と思われるケース」ではありませんよね?
@magicant さん、
たとえば↓のようなものがあります。
→後のコメント参照Charset
のコンストラクタのUnsupportedCharsetException
Stack#pop
のEmptyStackException
その他、→追記参照、他の例もIllegalArgumentException
をthrow
する多くのメソッドどうにも simple domain error と logic failure が恣意的に使い分けられているという印象を受けました。しかしそうなると、私にはそもそも「Simple domain errorが適切と思われるケースでLogic failureが採用されて」いるのが根本的な問題ではないように感じます。
例えば文字セットの名前を動的に生成するのであれば、不正な名前を
Charset
コンストラクタに渡してしまった場合に備えてUnsupportedCharsetException
をcatch
するコーディングが必要でしょう。しかしCharset
をインスタンス化するユースケースの大半は予め選ばれた固定の名前を指定するでしょうから、UnsupportedCharsetException
は発生するはずもなくcatch
しようと備えるのは無駄です。スタックも同じで、空かどうかわからないスタックから pop したのなら成功したかどうか確認が必要ですが、push したあと一度も pop していないことがコード上明らかなスタックから pop したときにわざわざ結果を確認するのは馬鹿らしい。
結局のところ、失敗を simple domain error として扱う必要があるのかそれともそもそも失敗するはずのない logic failure とみなして良いのかはそれぞれの呼び出し元の都合に左右されるのであって、呼び出される側が最初から「これは常に simple domain error だ」「これは常に logic failure だ」と決めておけるわけではないのです。 だからこそ Swift は「Simple domain errorやRecoverable errorをLogic failureとして扱う簡単な構文」を用意したわけでしょう?
Java は残念ながらそういううまい構文を発明できなかったので、Swift とは逆にいろいろな例外を logic failure の方に倒したのだと思います。つまり、呼び出し元がそれを logic failure とみなしたいのであれば、それは起こり得るはずのない例外 =
catch
すべからざる例外だから、何もせずただ無視すればいい。呼び出し元がそれを simple domain error や recoverable error とみなすのなら、catch
して適切にハンドリングすればいい。やっていることは C++ の頃と特に変わっていません。もし Java が Swift のようにもっといろいろなエラーを検査例外にしていたら、本来無視しておけばいい logic failure のハンドリングまで強制されることになり、「
catch
した例外をRuntimeException
に包んで投げ直す」みたいな本質的でないコードが今よりももっと増えていたはずです。そのようになっていないことは、果たして Java の欠点なのでしょうか。全てのエラーがこの4種類のいずれかに分類されるという話ですよね?
質問にYES/NOで答えていくとどのエラーに分類されるか分かるフローチャートを書くか、
マトリクスを書いて頂けるともっと分かりやすくなると思いました。
@magicant さん、
全体的に同意ですし、
というのも、現在の Java チュートリアルや Effective Java の方針としては正しいと思います。
僕が述べたかったのは、今、 Java をどう使うべきかという話ではなく、本文でも触れているように、言語設計段階でエラー分類について適切に考えられていなかったことが、そのような方針を作り出してしまい、結果的にエラー処理忘れを静的検査できるという魅力をフルに発揮できていないということです。
あと、
Charset
の例はご指摘の通り Logic failure の方が適切ですね。。最初特定のCharset
への変換に失敗した例を考えていたんですが、そこからたどって考えている内に頭がバグってました。打ち消し線で修正しておきます。pop
に関しては Swift のような軽量な(パフォーマンスの面からもハンドリングの面からも)構文があるのであれば Simple domain error として扱うべきだと思います。 Swift のような構文があれば、もし Logic failure 化したいなら↓で済みます。このように、 Simple domain error を Logic failure として扱わざるを得ない状況自体が言語設計の失敗ではないかという考えです。そして、 Java はあえてそれを選んだのではなく、
RuntimeException
がException
のサブクラスになっていることからも、言語設計時のエラーについての考えが甘かったんだろうと思います( 20 年前にそこまで考えることができたかということは別として、今から見れば甘かった部分があるという指摘です)。 Java を dis りたいわけではなく、むしろ、 Java の検査例外は批判されがちですが、ツメが甘かっただけで方向性としては間違っていなかったんだと考えています。@aosho235 さん、
によって
のように分類する話なのでこれ以上のものではないですが、あえてフローチャートにするなら↓でしょうか。
@yhara さん、
編集リクエストありがとうございます。 Twitter の引用の形にしているのには次のような理由があります。
まず、これが Swift Tweets というイベントのツイートまとめであり、そのイベントで発表する形を想定して作られたものだということです。発表形式によって最適な構成は異なるので、ツイートの引用の形でなかったとしても、それをそのまま長文に展開したのでは、長文として本来あるべき形にはならないだろうと考えています。試しに、↓のような Markdown 形式での投稿も試してみましたが、僕には構造がフラットすぎて読みづらかったです。
http://qiita.com/koher/private/58cf39b09f4e43725572
本来的に読みやすさを考えると、 Qiita の投稿やブログなどに適した構成で書き直すべきですが、それにはそれなりの労力がかかります。現実的にそれを確保できないので、それなら発表を保存するという意味で( Twitter はフロー型のサービスで流れてしまうので)ツイートをそのまま引用するのがいいんじゃないかと考えています。
もう一つは、 Swift Tweets では Twitter 上で発表を行うことによって、そのまま Twitter 上でリプをつけて補足説明をしたり議論をしたりできることを狙っています。ツイート引用の形になっていれば、クリックすることで簡単にその議論に飛べるということがあります。ただ、実際にはほとんどが引用リツイートでされてしまったために、( Twitter の UI の問題で)簡単にそれらにジャンプできないという問題はあります。次回以降は、参加者に引用リツイートを避けてリプでお願いするなどがいいかもしれません。
とはいえ、これだけ読みにくいという指摘を受けたので、良い改善策があれば実施したいです。本来的には投稿用に書き直し、関連する議論も整理できれば一番なんですが、それだと即時性に欠けますし(この投稿はイベント開催中、発表直後にされました。すぐに見返したい人もいるはずなので)、僕だけならともかくすべての発表者に要求するのは無理があるので・・・。↑のリンクの形式の方がまだマシだというならそれもいいのかもしれませんが、それはあくまでツイートと比べたらであって、 Qiita でいきなり↑の形式のページが出てきたら少し面食らいそうですし・・・。
togetterとかに上がってたらよくある感じだった気はします・・・。
「Java の RuntimeException はすべて回復不可能な Logic Failure を表すべきであり、それが Java 言語仕様の意図である」と書かれているところに違和感を感じます。(もし、そういうつもりでなかったらごめんなさい。僕の読解力が足りませんでした。)
Java 言語仕様によれば RuntimeException は回復可能な場合にも使えることを明示しています。
Effective Java にも『一般にキャッチするべきではなく』、『一般に回復が不能で、』と書かれていて、回復可能なケースもあることを暗示しています。
記事の後半で Java 言語仕様とは異なる仮説(「RuntimeException は Logic Failure と完全一致し、回復不能であり catch してはいけない』)を元にして議論が進んでいることが気になりましたのでコメントさせていただきました。
@kazzna さん
そうですね。 Togetter も検討しましたが、最終的には次のような理由から Qiita を選択しました。
結果として適切だったかわかりませんが。
@Shiozawa さん
コメントありがとうございます。
本文にも書いたように、この分類は絶対的なものではありません。 API の設計者がユースケースを想定して適切だろうと思われるものを選択するわけです。つまり、 Logic failure と想定して作られたものが、文脈に寄っては Simple domain error であってほしいケースというのは存在し、そのために Logic failure をハンドリング(または事前に起こらないように分岐)して Simple domain error として扱いたいケースは存在します。
たとえば、
(Array)IndexOutOfBoundsException
は多くのケースで Logic failure が適当ですが、 Simple domain error として番兵の代わりにIndexOutOfBoundsException
をcatch
するコードを書くこともできるわけです(パフォーマンスの問題は別として)。その上で、現代の Java では、
RuntimeException
は Logic failure として用いられるのが一般的です。 Effective Java の引用いただいた箇所もそうですし、 Java の公式チュートリアルでも次のように述べられています。そういう使い方が一般的に想定されているなら、そして、それが言語設計段階からの思想であれば、
RuntimeException
(Logic failure) とException
(Recoverable error) はクラスツリー上で分離されるべきだったのではないでしょうか。RuntimeException
を除くすべてのException
をまとめてcatch
する方法があってもよかったはずです。今はcatch (FooException|BarException e)
と書けるようになったからまだマシですが、それ以前はまとめてcatch
して共通のエラー処理を書くには共通の親例外クラスをcatch
するしかなく、それがException
であるケースも多かったはずです。そして、catch (Exception e)
するとRuntimeException
まで巻き込んでしまいます。もし言語設計段階でRuntimeException
が一般的に Logic failure として利用されることを考えていたなら、さすがにそのような設計はしないと思います。というのが僕の言いたかったことで、言語設計段階でそこまで考えていなかっただろうという仮説の理由です。