2012年5月16日水曜日

Scala Tips / Validation (12) - parse

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

Validationは、フォームやREST通信などでのパラメタチェックが典型的な利用例です。

その中でも、文字列を数値に変換する処理でのエラー検出は頻出処理です。Scalazでは、StringでparseIntメソッドなど以下のエラー検出用メソッドを提供しています。

  • parseBoolean
  • parseByte
  • parseShort
  • parseInt
  • parseLong
  • parseFloat
  • parseDouble

これらのメソッドの使い方についてみていきます。

parseInt

Int値を文字列から整数値に変換する処理はtoIntメソッドで行うことができますが、文字列のフォーマットがおかしかった場合、toIntメソッドはNumberFormatExceptionをスローします。NumberFormatExceptionをtry/catchで受けてエラー処理を行うのはかなり煩雑なコーディングになるのと、Monadicに処理できないのでできれば避けたいところです。Validationはこのような場合に便利なモナドです。

以下のようにtoIntメソッドを使うところで、parseIntメソッドを使います。そうすることでValidation[NumberFormatException, Int]が返ってきます。

def f(a: String): Validation[Throwable, Int] = {
  a.parseInt
}

フォーマットが正しかった場合は整数値、フォーマットエラーの場合にはデフォルト値として−1を返したい場合には以下のようにします。(Validation (1) - 値の取り出し)Validationにすることで、色々な技が使えるようになります。

def f(a: String): Int = {
  a.parseInt | -1
}

この他に、parseByteやparseDoubleなども同様の処理を行うことができます。

NonEmptyList

Validation (3) - NonEmptyList」で説明したようにエラー情報側にNonEmptyListを使うのがイディオムです。その場合は以下のようにliftFailNelメソッドを併用します。

def f(a: String): ValidationNEL[Throwable, Int] = {
  a.parseInt.liftFailNel
}
Applicative

エラー情報側にNonEmptyListを使うことによって、「Validation (10) - applicative」で説明したApplicativeを使うことができます。 

def f(a: String, b: String, c: String): ValidationNEL[Throwable, (Int, Float, Short)] = {
  (a.parseInt.liftFailNel |@| b.parseFloat.liftFailNel |@| c.parseShort.liftFailNel) tupled
}

正しい値の場合には以下のように動作します。

scala> f("10", "10.5", "100")
res60: scalaz.Scalaz.ValidationNEL[Throwable,(Int, Float, Short)] = Success((10,10.5,100))

フォーマットや値域に異常がある場合は以下のように動作します。

scala> f("10.5", "a", "100000")
res61: scalaz.Scalaz.ValidationNEL[Throwable,(Int, Float, Short)] = Failure(NonEmptyList(java.lang.NumberFormatException: For input string: "10.5", java.lang.NumberFormatException: For input string: "a", java.lang.NumberFormatException: Value out of range. Value:"100000" Radix:10))

NonEmptyListを使わない場合(正確にはエラー情報がMonoidでない場合)にはApplicativeで使用することができません。以下の関数はコンパイル・エラーとなります。

def f(a: String, b: String, c: String): Validation[Throwable, (Int, Float, Short)] = {
  (a.parseInt |@| b.parseFloat |@| c.parseShort) tupled
}

エラー情報にNonEmptyListを使わないと色々と弊害がでてくるので、Validationを使う場合にはNonEmptyListとセットにし、ValidationNELという型を使うのがイディオムとなっています。

同様に、エラー情報の中身にはExceptionではなくThrowableを使う方が便利です。この辺の事情は「Validation (6) - Exception」で説明しました。

ノート

parseIntメソッドを使わず自分でNumberFormatExceptionをハンドリングしてparseIntメソッド相当の処理を行う場合は以下のようなプログラムになります。scala.util.control.ExceptionのallCatchメソッドを使うのが簡便でよいでしょう。allCatchメソッドで返ってくるCatchオブジェクトのeitherメソッドを使ってEitherに変換し、さらにvalidationメソッドでValidationに変換します。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  validation(allCatch.either(a.toInt)).liftFailNel
}

allCatchメソッドでは、NumberFormatException以外のExceptionもキャッチしてしまうので、NumberFormatException限定にしたい場合にはcatchingメソッドを使います。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  val b = catching(classOf[NumberFormatException]).either(a.toInt)
  validation(b).liftFailNel
}

個人的には、このようなケースで個々のExceptionを頑張って拾ってもあまり益がないように思われるので(最後はエラー表示してタスクを終了するという処理になることが多いので)、おおむねallCatchを使います。

書き方

「allCatchメソッドで返ってくるCatchオブジェクトのeitherメソッドを使ってEitherに変換し、さらにvalidationメソッドでValidationに変換します」という処理を:

  • validation(allCatch.either(a.toInt)).liftFailNel

のように一行にまとめると処理が分かりづらくなります。このように関数型プログラミングでは、一行が長くなりがちなので実際のプログラミング時には工夫が必要になってきます。

一番安全なのは以下のように中間の値をval変数に入れていく方式です。美しくありませんが効果抜群で、デバッグもやりやすくなります。ボクもよく使っていて、中間のval変数名を考えるのが煩雑なので、a, b, cと連番で使うようにしています。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  val b = allCatch.either(a.toInt)
  validation(b).liftFailNel
}

一行にまとめた時に分かりづらくなる理由の一つに、処理の流れが右、左、右というように左右に移動しながら処理が進むということがあります。この問題に対する解として有力なのが、演算子「|>」です。「|>」を用いることで、処理が左から右へ一貫した方向にパイプライン上を流れるように進むように記述できます。この方式をとると一行が長くなっても苦労せずに処理を追うことができます。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  allCatch.either(a.toInt) |> validation liftFailNel
}

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿