Scalaで例外処理

kotohが2015/07/09 10:23:20に投稿

Scala 例外処理

try - catch - finally

Javaでおなじみのtry-catch-finallyを使う方法です。

try {
  func()
catch {
  case e: IOException => ...
  ...
finally {
  ... 
}

ただし、以下の点がjavaと異なります。

try, catchそれぞれのブロックは値を返す

finallyは値を返しません。

val n = try {
  "hoge".toInt
} catch {
  case e => 99
} finally {
  100
}
n: Int = 99 // 100ではない

非チェック例外

Scalaの例外は非チェック例外です。JavaのAPIを呼び出す時にthrowされる例外を全て捕捉しなくてもコンパイルエラーにはなりません。
@throwsアノテーションを用いると、throwsを付与したJavaバイトコードを生成することができます。

Option

Optionは値がない可能性を型で表現しています。
OptionはSome[T]とNoneという派生クラスを持ちます。

Scalaのマップでは、キーを指定してgetした場合、値があればSome(x), なければNoneが返ります。

scala> val map = Map("Japan" -> "Tokyo", "France" -> "Paris")
map: scala.collection.immutable.Map[String,String] = Map(Japan -> Tokyo, France -> Paris)
scala> map.get("France")
res1: Option[String] = Some(Paris)
scala> map.get("America")
res2: Option[String] = None

Optionの分解は次のように行います。

scala> map.get("key") match {
     | case Some(x) => x
     | case None => "none"
     | }
res7: String = none
scala> map.get("key").getOrElse("none")
res9: String = none

nullではなく、Optionを使いましょう。
でも、javaのAPIでnullを返す可能性がある場合はどうしたらよいでしょうか? javaのAPIはOptionを返してくれません。

Optionはabstractなクラスですが、コンパニトンオブジェクトを持っており、そのapplyは次のように実装されています。

  def apply[A](x: A): Option[A] = if (x == null) None else Some(x)
scala> Option(null)
res2: Option[Null] = None

となるので、Option(someJavaApi()) とすればjavaのAPIが返すnullをOptionで扱うことができます。

Either

ネットワーク、データベース、ファイルシステムなどの外部リソースにアクセスした結果を返す場合のように、エラーを含めた応答を返すときによく使われます。
EitherはRight[T, V]とLeft[T, V]という派生クラスを持ち、Rightには成功、Leftには失敗を入れます。

ファイルにアクセスする関数を定義します。

scala> def file(filename: String): Either[FileNotFoundException, FileInputStream] = {
     | 
     |   try {
     |     Right(new FileInputStream(filename))
     |   } catch {
     |     case e: FileNotFoundException => Left(e)
     |   }
     | }

次のような挙動をとります。

scala> file("exist.txt") // 存在するファイル
res1: Either[java.io.FileNotFoundException,java.io.FileInputStream] = Right(java.io.FileInputStream@2145433b)

scala> file("noexist.text") // 存在しないファイル
res2: Either[java.io.FileNotFoundException,java.io.FileInputStream] = Left(java.io.FileNotFoundException: no.text (No such file or directory))

RightかLeftはパターマッチで判定できます。

file(name) match {
  case Right(in) => // inを使ってファイル処理
  case Left(e) => println(e.toString) // エラー処理
}

Try

scala2.10.0以降で使える機能です。
TryはSuccess[T]とFailure[Throwable]という派生クラスを持ち、成功時にはSuccess、失敗時にはFailureが返ります。
Success, FailureはFutureを用いた非同期処理で利用されています。
Eitherと似ているように感じますが、Tryは処理が成功した場合はSuccess、例外が発生した場合はNonFatal(致命的ではない)な場合はFailureが返ります。

NonFatalも2.10.0で登場した機能です。
scala2.11.7ではscala.util.control.NonFatal.applyは次のように実装されています。

object NonFatal {
   def apply(t: Throwable): Boolean = t match {
     // VirtualMachineError includes OutOfMemoryError and other fatal errors
     case _: VirtualMachineError | _: ThreadDeath | _: InterruptedException | _: LinkageError | _: ControlThrowable => false
     case _ => true
   }
...

NonFatalではない、つまり致命的だと判断されるのは以下の例外です。

  • VirtualMachineError
  • ThreadDeath
  • InterruptedException
  • LinkageError
  • ControlThrowable

これらの例外が発生した場合は、Failureではなくそのまま例外がスローされます。
NonFatalな例外(Failure)は基本的にアプリケーションでエラー通知する握り潰すデフォルト値を設定するなどの対応をとるべき例外です。
NonFatalかどうかの判定は次のように行います。

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.")
}

Tryは内部で、NonFatalかどうかでキャッチするべき例外を区別しています。
例外処理を行う場合は、NonFatalによる判定に合わせておいた方がよさそうです。

Try, Success, Failureは次のように使います。

scala> import scala.util.{Try, Success, Failure}
import scala.util.{Try, Success, Failure}
scala> val result = Try(10/5)
result: scala.util.Try[Int] = Success(2)

scala> val result = Try(10/0)
result: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)
scala> Try(1/0) match {
     |   case Success(v) => println(v)
     |   case Failure(e) => println(e.toString)
     | }
scala> val sum = for {
     |   int1 <- Try(Integer.parseInt("1"))
     |   int2 <- Try(Integer.parseInt("2"))
     | } yield {
     |   int1 + int2
     | }
sum: scala.util.Try[Int] = Success(3)

scala> val sum = for {
     |   int1 <- Try(Integer.parseInt("1"))
     |   int2 <- Try(Integer.parseInt("foo"))
     | } yield {
     |   int1 + int2
     | }
sum: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "foo")

FailureからSuccessにリカバリすることもできます。

scala> val result = Try(10/0)
result: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)
scala> result.recover {
     |   case e: ArithmeticException => 0 // 0除算してしまった場合は0にする
     | }
res11: scala.util.Try[Int] = Success(0)

まとめ

  • try-catch-finally は基本
  • nullは使わずOptionで
  • エラーの詳細を伝えたい時はEither
  • 例外をラップする時はTry