ScalazのOptionはMonoidとしても動作します。Option内にMonoidを格納している場合、Optionを|+|演算することでOption内に格納しているMonoidも自動的に|+|演算が適用されます。
scala> 1.some |+| 2.some res10: Option[Int] = Some(3) scala> 1.some |+| none res11: Option[Int] = Some(1) scala> none |+| 2.some res12: Option[Int] = Some(2) scala> none[Int] |+| none[Int] res14: Option[Int] = None
とても便利な機能ですが、残念ながらValidationはこの機能をもっていません。
そこでValidationそのものをMonoidとして使用することを考えてみましょう。
Monoidの前段階としてSemigroupがあります。Semigroupは半群を表す型クラスです。Monoidの演算子|+|はSemigroupで定義されています。
ValidationをMonoid化するための準備として、ValidationをSemigroup化します。
ValidationNELThrowables
ValidationをSemigroup化するための実装として以下を作成しました。トレイトValidationNELThrowablesまたはオブジェクトValidationNELThrowablesに暗黙パラメタとして定義されているValidationNELのSemigroup型クラスインスタンスを読み込んで使用します。
trait ValidationNELThrowables { type NT = NonEmptyList[Throwable] type VNT[A] = Validation[NT, A] implicit def VNTSemigroup[A: Semigroup]: Semigroup[VNT[A]] = semigroup((a, b) => { (a, b) match { case (Success(va), Success(vb)) => Success(va |+| vb) case (Success(va), Failure(_)) => Success(va) case (Failure(_), Success(vb)) => Success(vb) case (Failure(ea), Failure(eb)) => Failure(ea |+| eb) }}) } object ValidationNELThrowables extends ValidationNELThrowables
Validationのコンテナ自身を結合する戦略としてはAND演算とOR演算がありますが、ここではOptionの仕様にあわせてOR演算を採用しています。
使ってみる
準備として型クラスインスタンスを読み込みます。
scala> import ValidationNELThrowables._ import ValidationNELThrowables._
Success同士のモノイド演算を行うと、Success内に格納されているMonoid同士にモノイド演算が適用されます。
scala> 1.success[NT] |+| 2.success[NT] res1: scalaz.Validation[ValidationNELThrowables.NT,Int] = Success(3)
一方がSuccess、もう一方がFailureの場合はSuccessの方が使用されます。
scala> 1.success[NT] |+| new IllegalArgumentException("bad").failNel res2: scalaz.Validation[ValidationNELThrowables.NT,Int] = Success(1) scala> (new IllegalArgumentException("bad").failNel[Int]: Validation[NT, Int]) |+| 2.success[NT] res19: scalaz.Validation[ValidationNELThrowables.NT,Int] = Success(2)
両方がFailureの場合はFailureになります。
scala> (new IllegalArgumentException("bad 1").failNel[Int]: Validation[NT, Int]) |+| new IllegalArgumentException("bad 2").failNel[Int] res21: scalaz.Validation[ValidationNELThrowables.NT,Int] = Failure(NonEmptyList(java.lang.IllegalArgumentException: bad 1, java.lang.IllegalArgumentException: bad 2))
型の指定
「new IllegalArgumentException("bad").failNel[Int]」はValidation[NT, Int]と型が合わないらしく演算の左側に持ってくると以下のようにエラーとなります。
scala> new IllegalArgumentException("bad").failNel[Int] |+| new IllegalArgumentException("bad").failNel[Int] <console>:19: error: could not find implicit value for parameter s: scalaz.Semigroup[scalaz.Scalaz.ValidationNEL[java.lang.IllegalArgumentException,Int]] new IllegalArgumentException("bad").failNel[Int] |+| new IllegalArgumentException("bad").failNel[Int] ^
この問題を回避するためには「(new IllegalArgumentException("bad").failNel[Int]: Validation[NT, Int])」と型を指定する必要があるようです。
以下のように関数引数の場合は、「new IllegalArgumentException("bad").failNel[Int]」はValidation[NT, Int]と認識されます。
def f[A: Monoid](a: VNT[A], b: VNT[A]): VNT[A] = { import ValidationNELThrowables._ a |+| b }
このため、以下のように型指定をしなくても動作します。プログラミング時にはこの形に持ち込めば、余分な型指定を回避することができます。
scala> f(1.success, 2.success) res4: ValidationNELThrowables.VNT[Int] = Success(3) scala> f(1.success, new IllegalArgumentException("bad").failNel) res7: ValidationNELThrowables.VNT[Int] = Success(1) scala> f(new IllegalArgumentException("bad").failNel, 2.success) res6: ValidationNELThrowables.VNT[Int] = Success(2) scala> f(new IllegalArgumentException("bad 1").failNel[Int], new IllegalArgumentException("bad 2").failNel[Int]) res9: ValidationNELThrowables.VNT[Int] = Failure(NonEmptyList(java.lang.IllegalArgumentException: bad 1, java.lang.IllegalArgumentException: bad 2))
ノート
あるクラスをSemigroupにすることが可能になるためには、型パラメータが1つであることという条件があります。Validationの型パラメータ数は2で、これがValidationがMonoidとして定義されていない理由の一つだと思います。
幸いなことに本ブログではValidationのFailure側はNonEmptyList[Throwable]を基本に考えているので、型パラメータの一つをNonEmptyList[Throwable]に固定することができます。このため、ValidationをMonoid化するための条件が整っています。
諸元
- Scala 2.9.2
- Scalaz 6.0.4
0 件のコメント:
コメントを投稿