2012年10月25日木曜日

Scala Tips / Scala 2.10味見(17) - NonFatal

Scala 2.10で導入される機能の中で、地味だけどちょっと注目しておきたいものとしてscala.util.control.NonFatalがあります。

Scalaプログラミングでも例外のハンドリングは引き続き重要項目です。scala.util.control.NonFatalは例外の分類に関するユーティリティ・オブジェクトです。

Javaの例外は大きく以下の4つに分類できます。

java.lang.Error
致命的なエラー
java.lang.RuntimeException
非検査例外
Runtime以外のException
検査例外
それ以外のThrowable
特殊用途

java.lang.Error(以下Error)とjava.lang.RuntimeException(以下RuntimeException)はどちらもthrows句で宣言しないでも発生する可能性がある例外です。その心は、システム側の致命的なエラー(Error)またはアプリケーションのバグ(RuntimeException)なので例外をキャッチしてもリカバリしようがないので、精密な例外ハンドリングが必要な場合以外は無視してよい、ということでしょう。

ご存知の通り、Scalaでは検査例外の機能がなくなってしまったので、java.lang.RuntimeExceptionとそれ以外のExeptionの違いがプログラミングに反映されなくなってしまう傾向は出てくるでしょう。

逆に、例外の使い方に対する制約もなくなったので、アプリケーションのニーズに従って例外を分類して使用するようにしておくと良いと思います。

例外に対する処理

例外に対する処理は概ね以下のものになります。

  1. エラー処理は放棄して上位に例外をそのまま上げる。
  2. 利用者にエラーを通知したりデフォルト値を返すなどして普通の処理に戻す。
  3. 例外を握りつぶす。(ログは取る事が多い)
  4. リトライする。

JavaではErrorとRuntimeExceptionは「1. エラー処理は放棄して上位に例外をそのまま上げる。」が基本操作となる例外と想定していると考えられます。

ここで問題となるのは、JavaではError扱いの例外であっても必ずしもシステム側の致命的な障害とは限らないということです。たとえば、StackOverFlowErrorは再帰呼び出しでスタックがあふれた時に発生するケースが多いですが、これは基本的にはプログラムのバグと考えてよいものです。

このため、このようなケースでは「1. エラー処理は放棄して上位に例外をそのまま上げる。」は適切ではなく、「2. 利用者にエラーを通知したりデフォルト値を返すなどして普通の処理に戻す。」や「3. 例外を握りつぶす。(ログは取る事が多い)」といった処理を選択する必要があります。

この切り分けの機能を提供しているのがNonFatalです。

NonFatalでは以下の例外を致命的なエラーと判定します。

  • VirtualMachineError(StackOverFlowError以外)
  • ThreadDeath
  • InterruptedException
  • LinkageError
  • ControlThrowable
  • NotImplementedError

これ以外の例外はErrorであっても、致命的例外とはみなさないということになります。

ControlThrowable

致命的エラーか否かという観点とは別に、制御フローの実装技術として例外を使うケースがあります。Scalaの基本ライブラリではscala.util.control.ControlThrowableがそれに当たります。この例外に対しては形の上で致命的エラーと判定します。こうすることで、アプリケーションがControlThrowableを誤ってキャッチしてしまうことを防ぎます。

使い方

NonFatalのScaladocでは、以下のような使い方を紹介しています。

try {
     // dangerous stuff
   } catch {
     case NonFatal(e) => log.error(e, "Something not that bad.")
    // or
     case e if NonFatal(e) => log.error(e, "Something not that bad.")
   }

つまり、NonFatalと判定される例外はキャッチしてしかるべきエラー処理をしますが、それ以外の例外はアプリケーションでのエラー処理は諦めるという使い方ですね。

Try

NonFatalが重要な理由の一つとして、scala.util.Tryがキャッチすべき例外の判定にNonFatalを使用していることがあります。

Tryが必ずしもすべての例外をキャッチしてモナドの文脈に乗せてくるわけではないという点は注意しておくべきです。致命的と判断された例外はそのまま例外のメカニズムに乗って上位に渡っていきます。

もう一点、TryがScala 2.10以降のエラー処理の重要なキーパーツになると思いますが、そのTryが致命的例外とそうでない例外の切り分けに使用しているロジックは、アプリケーション側でもできるだけ合わせておいたほうが得策です。そういう意味で、特別な考察が必要なケース以外はNonFatalを使うようにするという方針でプログラミングに臨むとよいでしょう。

諸元

  • Scala 2.10.0-RC1

0 件のコメント:

コメントを投稿