2012年6月8日金曜日

Scala Tips / Validation (26) - fold monoid or

Validationの集りに対して畳込みを行う場合、Failureの扱いには以下の2つの選択肢があります。

  • Failureが一つでもあれば畳込み全体をFailureにする。
  • Failureは飛ばしてSuccessのみを畳込む。

前者はValidationのScalaモナド、Scalaz Applicative演算の基本動作なので普通にモナド演算やApplicative演算を行うと自動的に処理が行われます。

問題は後者で、これはモナドやApplicative側で自動的に処理してくれないので、スクラッチでロジックを書く必要があります。

ここでは、Validationの格納要素をMonoidに限定し、畳込みをMonoidの加算演算の場合の実装方法について考えます。

前者の場合はsequenceメソッドとsumrメソッドの組合せを使えば、fold系メソッドを使わずにシンプルに実装する手段もあったわけですが、後者の場合はfold系メソッドを使って地道に実装する必要があります。ただしValidationの場合は、こういった用途に使える「>>*<<」メソッドを提供しているので、これを活用することにします。

課題

Monoid型の値を格納したValidationのリスト上で、Monoid型の値をモノイドの加法演算で畳込んで結果の値を格納したValidationを生成します。ただし、ValidationがFailureがあった場合には、Failureは飛ばしてSuccessのものだけを畳み込み結果をSuccessにします。

具体的には、以下の関数を作ります。

  • f[T: Monoid](a: List[ValidationNEL[Throwable, T]): ValidationNEL[Throwable, T]

「>>*<<」メソッド

まず、Validationの「>>*<<」メソッドの動作確認をしておきましょう。

まず、Successを2つ、Failureを1つ用意します。

scala> val s1 = "1".parseInt.liftFailNel
s1: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(1)

scala> val s2 = "2".parseInt.liftFailNel
s2: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(2)

scala> val f = "x".parseInt.liftFailNel
f: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Failure(NonEmptyList(java.lang.NumberFormatException: For input string: "x"))

前回の処理を行うと要素の一つがFailureであるため畳込み全体がFailureになります。これはsequenceメソッドによるValidationのApplicative演算で、Validationが一つでもFailureであると自動的に演算全体がFailureになるためですね。

scala> type VNT[A] = ValidationNEL[Throwable, A]
defined type alias VNT

scala> List(s1, f, s2).sequence[VNT, Int].map(_.sumr)
res41: scalaz.Validation[scalaz.NonEmptyList[Throwable],Int] = Failure(NonEmptyList(java.lang.NumberFormatException: For input string: "x"))

それでは「>>*<<」メソッドの動きを試してみましょう。

2つのSuccessを「>>*<<」すると、Success上に格納されているMonoid値が加算されます。「>>*<<」メソッドはある意味Monoidの「|+|」メソッドと同じような動きをイメージするとよいでしょう。Monoidの加算は「|+|」、Monoidを格納したValidationの加算は「>>*<<」、ですね。

scala> s1 >>*<< s2
res28: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(3)

「>>*<<」メソッドは単に加算するだけでなく、Failureは読み飛ばすという機能を持っています。以下では、片方にFailureを指定していますが、これを読み飛ばしもう片方のSuccessが結果として返っています。

scala> s1 >>*<< f
res29: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(1)

scala> f >>*<< s2
res30: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(2)

「>>*<<」メソッドは以下のように複数連結することができます。この場合には、Failureは読み飛ばし、Successの集りを加算します。

scala> s1 >>*<< s2 >>*<< f
res31: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(3)

scala> s1 >>*<< f >>*<< s2
res32: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Success(3)

参考情報ですが、上の処理に対応するApplicativeの演算は以下になります。Applicativeの場合は一つでもFailureがあれば、演算全体がFailureになります。

scala> (s1 |@| f |@| s2)(_ |+| _ |+| _)
res34: scalaz.Validation[scalaz.NonEmptyList[NumberFormatException],Int] = Failure(NonEmptyList(java.lang.NumberFormatException: For input string: "x"))

実装

Validationの「>>*<<」メソッドを使った実装は以下になります。

あらかじめValidationの要素数が決まっている場合は、前述のように「>>*<<」メソッドをつないでいけばよいのですが、任意個数の場合にはfold系メソッドを使って畳込みを行う必要があります。

def f[T: Monoid](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, T] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  a.foldLeft(mzero[T].pure[VNT]) {
    (a, x) => a >>*<< x
  }
}

Validationに格納されたMonoidの加算は「>>*<<」メソッド内で行ってくれるので「>>*<<」メソッドを「|+|」メソッド的な感覚で畳込みに使えばOKです。

畳込みの初期値にはMonoidの零元(mzero関数)をValidation化(pure関数)したものを使います。初期値がSuccessなので、仮に要素がすべてFailureであっても「>>*<<」メソッドによる畳込みは必ずSuucessになります。

動作確認

まず、テスト用のデータを用意します。sは全部Success、fsは一つだけSuccessとFailureが混在、ffは全部Failureです。

scala> val s = List(1.success, 2.success, 3.success)
s: List[scalaz.Validation[Nothing,Int]] = List(Success(1), Success(2), Success(3))

scala> val fs = List(1.success, new IllegalArgumentException("bad").failNel, 3.success)
fs: List[scalaz.Validation[scalaz.NonEmptyList[java.lang.IllegalArgumentException],Int]] = List(Success(1), Failure(NonEmptyList(java.lang.IllegalArgumentException: bad)), Success(3))

scala> val ff = List(new IllegalArgumentException("bad").failNel[Int], new IllegalArgumentException("bad").failNel[Int], new IllegalArgumentException("bad").failNel[Int])
ff: List[scalaz.Scalaz.ValidationNEL[java.lang.IllegalArgumentException,Int]] = List(Failure(NonEmptyList(java.lang.IllegalArgumentException: bad)), Failure(NonEmptyList(java.lang.IllegalArgumentException: bad)), Failure(NonEmptyList(java.lang.IllegalArgumentException: bad)))

全部Successはすべての要素が加算されたものがSuccessで返ってきます。

scala> f(s)
res8: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(6)

一部Failureがある場合は、successのもののみが加算されたものがSuccessで返ってきます。

scala> f(fs)
res9: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(4)

すべてFailureの場合は、Monoidの零元がSuccessで返ってきます。

scala> f(ff)
res25: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(0)

エラーにしたい場合

課題では、前要素がFailureの場合には零元の値が返ってきましたが、演算全体をエラーにしたいケースもあると思います。

その場合には、以下のように初期値にFailureを指定します。初期値がFailureなので、要素もすべてFailureの場合「>>*<<」メソッドに対する引数がすべてFailureになるためですね。

def f[T: Monoid](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, T] = {
  val i: Throwable = new IllegalArgumentException("all bad")
  a.foldLeft(i.failNel[T]) {
    (a, x) => a >>*<< x
  }
}

初期値に適切な型を設定するのが案外大変です。ここでは、「val i: Throwable = new IllegalArgumentException("all bad")」として、変数iの型をThrowableに指定した上で「i.failNel[T]」として初期値の型の設定をしています。

動作確認

すべての要素がFailureの場合には、処理全体もFailureとなるようになりました。

scala> f(ff)
res27: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Failure(NonEmptyList(java.lang.IllegalArgumentException: all bad, java.lang.IllegalArgumentException: bad, java.lang.IllegalArgumentException: bad, java.lang.IllegalArgumentException: bad))

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿