2016年1月31日日曜日

[SDN] 例外とNothing

try/catchで例外処理を書く場合、以下のように例外に対する共通処理を羅列する形になることがあります。

すべての例外に対して同じ処理を行う場合には、大本のjava.lang.Exception(場合によってはjava.lang.Throwable)でキャッチすればよいですが、以下のように例外毎に少しずつ処理が異なる場合には羅列せざるを得ません。

def doSometing(): String = {
  try {
    doSomesing()
  } catch {
    case e: FileNotFoundException =>
      log("file not found")
      throw e
    case e: InterruptedIOException =>
      log("intterupted io")
      throw e
    case e: IOException =>
      log("io")
      throw e
    case NonFatal(e) =>
      log("non fatal")
      throw e
  }
}

上記のプログラムでは以下の処理が例外の共通処理になります。

log("file not found")
throw e

「例外毎に異なったメッセージをログに出力して、受け取った例外を再送出する処理」ですが、ログに出力するメッセージの文言が例外毎に変わってきます。

共通処理化

「例外毎に異なったメッセージをログに出力して、受け取った例外を再送出する処理」を関数化して再利用するために以下の関数を作ってみました。

この処理は例題なので単純なものになっていますが、製品開発ではもっと細かくて複雑な処理が必要になってくる可能性があります。また、システム共通のエラー処理を変更する場合には、エラー処理のロジックが一箇所に集まっていた方が取り回しが楽ということもあります。

def handleError(message: String, e: Throwable) {
  log(message)
  throw e
}

この関数は以下のように例外処理に組み込みます。

try {
    doSomesing()
  } catch {
    case e: FileNotFoundException =>
      handleError("file not found", e)
    case e: InterruptedIOException =>
      handleError("intterupted io", e)
    case e: IOException =>
      handleError("io", e)
    case NonFatal(e) =>
      handleError("non fatal", e)
  }

一見これでよいように思いますが、実は以下のようなコンパイルエラーが出てしまいます。

...
[error]  found   : Unit
[error]  required: String
[error]         handleError("file not found", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("intterupted io", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("io", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("non fatal", e)
[error]                    ^
[error] four errors found

その理由は、handleError関数の返却値がUnitであるため、例外処理を行っているdoSomething関数の返却値Stringと型が合わないためです。

この問題の対策が今回のテーマです。

Stringを返す

利用者側の関数の返却値とhandleError関数の返却値が合わない問題を解決する方法として、返却する型を明示したhandleError関数を用意する方法があります。

たとえば以下のようなhandleErrorString関数を用意する方法です。

def handleErrorString(message: String, e: Throwable): String = {
  log(message)
  throw e
}

しかし、この方法を採ると必要な型ごとに1つ関数を用意する必要があります。用途によっては考えられる解決策ですが、汎用的に適用できる方法ではありません。

型パラメータ

1つの解としては以下のように型パラメータを使う方法があります。

def handleError[T](message: String, e: Throwable): T = {
  log(message)
  throw e
}

handleError関数の使用元が必要とする型が自動的に型Tに設定されhandleError関数の返却値となります。このためコンパイルエラーになりません。

Nothing

前述の型パラメータを使う方法でもよいのですが、Scalaにはこのような目的に使用できるNothingという型があります。

Nothingはすべての型のサブクラス、という特殊な位置付けの型です。一種のワイルドカードのような型です。

以下のようにhandleError関数の返却値をNothingとすると、doSomething関数がどのような型を返すことになっても、そのまま利用することができます。

def handleError(message: String, e: Throwable): Nothing = {
  log(message)
  throw e
}

この例のように最後に例外を送出する処理を行う関数は、実際には値は返すことはないので、返却値の型をNothingしても問題はなく、利用範囲は大きく広がります。

今回の問題に対しては前述の型パラメータを使う方法でも対処可能ですが、Nothingを使った方がより意図が明確になると思います。

参考

未実装のメソッドを定義するのに便利な「???」メソッドも返却値にこのNothingを使っています。「???」メソッドの定義は以下のようになっています。

def ??? : Nothing = throw new NotImplementedError

諸元

  • Java 1.7.0_75
  • Scala 2.11.7