2012年7月11日水曜日

Scala Tips / Validation (35) - Monoid

Validation (34) - Semigroup」ではValidationをSemigroup化しました。その結果、演算子|+|が使用できるようになりました。

しかしSemigroup化しただけではMonoidの機能を十分に活用することはできません。たとえばsumrメソッドはMonoidが対象なので、以下のようにエラーになってしまいます。この他foldMapメソッドなどSemigroupではなくMonoidを対象にしたメソッドは多数存在しているので、できればSemigroupではなくMonoid化しておくのが得策です。

scala> List(1.success[NT], 2.success[NT], 3.success[NT]).sumr
<console>:19: error: could not find implicit value for parameter m: scalaz.Monoid[scalaz.Validation[ValidationNELThrowables.NT,Int]]
              List(1.success[NT], 2.success[NT], 3.success[NT]).sumr
                                                                ^

ScalazのMonoidはZeroとSemigroupの両型クラスを継承しています。前回はValidationの型クラスSemigroupインスタンスを作成してValidationを型クラス化しました。これに加えて型クラスZeroインスタンスを作成すれば、ValidationのZero化が行われ、同時にMonoid化も達成されます。

Validationの型クラスZeroインスタンスを追加したコードが以下になります。暗黙値VNTZeroとしてValidation[NonEmptyList[Throwable], A]に対する型クラスZeroインスタンスを定義しています。

trait ValidationNELThrowables {
  type NT = NonEmptyList[Throwable]
  type VNT[A] = Validation[NT, A]

  implicit def VNTZero[A: Zero]: Zero[VNT[A]] = zero(mzero[A].success)
  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

VNTZeroでは、Validation[NonEmptyList[Throwable], A]がZeroであるためには、格納するオブジェクトもZeroであることを条件にしています。そしてValidation[NonEmptyList[Throwable], A]の零元は、格納するオブジェクトの零元を作成し、これをSuccessに格納にしたものになります。

動かしてみる

Validation (34) - Semigroup」でValidationのSemigroup化、そして今回ValidationのZero化を行いました。SemigroupかつZeroのオブジェクトは自動的にMonoidとなります。つまりValidationをMonoidとして使用できるようになったわけです。その結果Monoidを対象としたさまざまなメソッドでValidationを使用できるようになりました。

sumrメソッドを使った結果は以下になります。

scala> List(1.success[NT], 2.success[NT], 3.success[NT]).sumr
res25: scalaz.Validation[ValidationNELThrowables.NT,Int] = Success(6)

foldMapメソッドも普通に使えるようになりました。

scala> List(1, 2, 3).foldMap(_.success[NT])
res39: scalaz.Validation[NT,Int] = Success(6)

ノート

Optionの型クラスZeroインスタンスの仕様では零元としてNoneが返されます。

scala> mzero[Option[Int]]
res2: Option[Int] = None

Optionの型クラスSemigroupインスタンスの仕様では、NoneとSomeをモノイド演算(演算子|+|)するとSomeとなります。このためNoneは零元(単位元)として機能しています。

scala> 1.some |+| 2.some
res9: Option[Int] = Some(3)

scala> 1.some |+| none
res10: Option[Int] = Some(1)

scala> none |+| 2.some
res11: Option[Int] = Some(2)

scala> 1.some |+| 2.some
res12: Option[Int] = Some(3)

Validationの実装でも、この選択を取ってもよいのですが:

Failure(NonEmptyList(java.lang.Throwable: zero))

が単位元というのも少し違和感があるので、今回の実装では:

scala> mzero[VNT[Int]]
res17: ValidationNELThrowables.VNT[Int] = Success(0)

となるようにしました。

Scalazの選択が前者の方式になっているので、数学的にはその方が筋がよい可能性がありますが、そのあたりの事情はよくわかりません。情報をいただけると助かります。

>>*<<メソッド

Validationが格納するオブジェクトがMonoidで、Validationのコンテナの結合戦略がOR演算である場合、Validationの>>*<<メソッドを使うことができます。(「Validation (26) - fold monoid or」)

このためSemigroupの実装は>>*<<メソッドを使うとより簡潔になります。>>*<<メソッドを使って改良したValidationNELThrowablesは以下になります。

trait ValidationNELThrowables {
  type NT = NonEmptyList[Throwable]
  type VNT[A] = Validation[NT, A]

  implicit def VNTZero[A: Zero]: Zero[VNT[A]] = zero(mzero[A].success)
  implicit def VNTSemigroup[A: Semigroup]: Semigroup[VNT[A]] = semigroup((a, b) => a >>*<< b)
}

object ValidationNELThrowables extends ValidationNELThrowables

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿