2012年4月23日月曜日

Scala Tips / Validation (6) - Exception

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。Validationを使ってOptionと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。

前回はValidationから値を取り出す際に、以下の表の演算を行う方法について考えました。「Option (10) - Exception」や「Either (5) - Exception」で扱った課題のValidation版です。

条件結果
Validation[A, B]がSuccess[A, B]でBに有効な値が入っているSuccess[A, C]
Validation[A, B]がSuccess[A, B]でBに無効な値が入っているFailure[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

今回は「Success[A, B]に無効な値が入っている」の判定で例外を使用するケースです。

以下では、ValidationNEL[Throwable, String]からValidationNEL[Throwable, Int]への変換を例に考えます。Validationに入っているStringがIntに変換できる場合は有効となり、Success[NonEmptyList[Throwable], Int]が処理結果となります。一方、変換できない場合は無効となりFailure[NonEmptyList[Throwable], Int]が処理結果となります。

ここで、StringからIntへの変換ができない事の判定に例外を使用します。(String#toIntメソッドがNumberFormatExceptionをスロー。)

(分類の基準)

Java風

if式でValidation#isSuccessを使って成功の有無を判定します。

文字列が整数値に合致しなかった場合String#toIntメソッドがeNumberFormatExceptionをスローするので、これをtry/catch文でキャッチしています。

引数がFailureの場合とNumberFormatExceptionをキャッチした時がFailure(NonEmptyList(Throwable))、toIntで整数値が得られた場合はSuccess(Int)が演算結果となります。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  if (a.isSuccess) {
    val s = a.asInstanceOf[Success[NonEmptyList[Throwable], String]]
    try {
      Success(s.a.toInt)
    } catch {
      case e: NumberFormatException => {
        Failure(NonEmptyList(e))
      }
    }
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, Int]]
  }
}

Scala風

match式を使うと以下のようになります。try/catch文が入るとプログラムの見通しはJava風とそれほど変わらなくなります。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a match {
    case Success(s) => {
      try {
        Success(s.toInt)
      } catch {
        case e: NumberFormatException => {
          Failure(NonEmptyList(e))
        }
      }
    }
    case Failure(e) => Failure(e)
  }
}

Scala

ValidationはScalazの機能ですが、ここではScala的なプログラミングをしてみます。

Either (5) - Exception」で説明したように、mapメソッドを使うのは得策ではないので、flatMapメソッドを用います。flatMapメソッドを使うことで、成功の文脈から失敗の文脈への切り替えを行うことができるようになります。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a.flatMap { b =>
    try {
      Success(b.toInt)
    } catch {
      case e: NumberFormatException => {
        Failure(NonEmptyList(e))
      }
    }
  }
}

処理が成功した場合は「Success(b.toInt)」で成功の文脈を維持、例外が発生した場合は「Failure(NonEmptyList(e))」で失敗の文脈に切り替えます。

scala.util.control.Exception

例外をキャッチしてEitherやOptionに変換する機能がオブジェクトscala.util.control.Exceptionに用意されています。よく使うのがcatching関数とallCatch関数です。

catching関数は、キャッチする例外を列挙して指定することができます。catching関数から返されるCatchオブジェクトのeitherメソッドで例外の発生をEitherに変換することができます。

ここで得られたeitherを「Either (20) - Validationと相互変換」で説明した方法でValidationに変換します。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a.flatMap { b =>
    catching(classOf[NumberFormatException]) either {
      b.toInt
    } fold (e => Failure(NonEmptyList(e)), Success(_))
  }
}

Either[Throwable, Int]をValidation[Throwable, Int]に変換する場合はeitherメソッドの値をそのまま使えばよいのですが、ThrowableをNonEmptyList[Throwable]に変換しなければならないので、Either#foldメソッドを用いています。

allCatch

多くの場合はNumberFormatException以外の例外が発生しても関数のエラーとして返して大丈夫だと思われるので、すべての例外をキャッチするallCatching関数を使う方法も有力です。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a.flatMap { b =>
    allCatch either {
      b.toInt
    } fold (e => Failure(NonEmptyList(e)), Success(_))
  }
}

ここで得られたeitherを「Either (20) - Validationと相互変換」で説明した方法でValidationに変換します。

Scalaz

「Scala」と基本的な構造は同じですが、Scalazの機能を利用するとよりコンパクトに記述することができます。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a >>= { b =>
    try {
      Success(b.toInt)
    } catch {
      case e: NumberFormatException => {
        e.failNel
      }
    }
  }
}

flatMapメソッドを>>=メソッドで、「Failure(NonEmptyList(e))」を「e.failNel」で記述しました。

scala.util.control.Exception

cathingメソッドで例外をハンドリングする場合は、以下のようになります。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a >>= { b =>
    catching(classOf[NumberFormatException]) either {
      b.toInt
    } fold (_.failNel, _.success)
  }
}

Either#foldメソッドを使う際に「fold(_.failNel, _.success)」とコンパクトに記述できるのがScalaz的です。

allCatch

allCatchのScalaz版は以下のようになります。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a >>= { b =>
    allCatch either {
      b.toInt
    } fold (_.failNel, _.success)
  }
}

ノート

scala.util.control.Exceptionのcatchingメソッド、allCatchメソッドで返されるCatchオブジェクトのeitherメソッドのシグネチャがThrowable固定になっています。

このため、catchingやallCatchを使用する場合、ThrowableではなくExceptionを例外クラスとして使用すると「Either (5) - Exception」で説明したように、色々なところでキャストが必要となりかなり不便です。

そこで、例外クラスを統一的に扱う場合はThrowableを使うのがイディオムとなっています。

今回のValidationの例では、この点を見越してThrowableを使っているので、処理をスムーズに記述できています。

追記(2012-06-21)

ValidationをScalazのMonadとして>>=メソッドを使うには、「Validation (11) - モナド」で説明したように「import Validation.Monad._」のおまじないが必要です。このため「Scalaz」の項で採用している>>=メソッドはこの場合はあまり便利ではありません。素直にflatMapメソッドを使う方がよいでしょう。

throwsメソッド

この記事を書いた時点ではscala.util.control.ExceptionのallCatchを使うのがベストだと思っていたのですが、「Validation (32) - Function0」の記事にあるように「Function0[T]」(「() => T」)のthrowsメソッドを使う方がより便利です。

例外を細かくキャッチし分けたい場合は、引き続きscala.util.control.Exceptionのcatchingが有効です。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿