2012年7月12日木曜日

Scala Tips / Validation (36) - Reducer

Validation (35) - Monoid」でValidationをMonoidとして定義したので、Reducerで直接使うことができるようになりました。

たとえば、以下のように使用することができます。

scala> List(1.success[NT], 2.success[NT], 3.success[NT]).foldReduce(implicitly[Foldable[List]], Reducer(x => x))
res20: scalaz.Validation[ValidationNELThrowables.NT,Int] = Success(6)

Monoid - 新規作成」、「Reducer (5) - 演算Monoid」では、Monoid Averageを使ってPersonの集まりの平均年齢を計算しました。今回は、Monoid化したValidationを適用してみます。

MonoidであるAverageは以下になります。

case class Average(total: Int, count: Int) {
  def +:(a: Int) = Average(total + a, count + 1)
  def :+(a: Int) = Average(total + a, count + 1)
  def +(a: Average) = Average(total + a.total, count + a.count)
  def value: Float = if (count == 0) 0 else total.toFloat / count
}

trait Averages {
  implicit def AverageZero: Zero[Average] = zero(Average(0, 0))
  implicit def AverageSemigroup: Semigroup[Average] = semigroup((a, b) => a + b)
}

object Averages extends Averages

Personの定義とテスト用のインスタンスは以下になります。

case class Person(name: String, age: Int)
val taro = Person("Taro", 35)
val hanako = Person("Hanako", 28)
val saburo = Person("Saburo", 43)

type NT = NonEmptyList[Throwable]

val persons = List(taro.success[NT], hanako.success[NT], saburo.success[NT])

使ってみる

Validationに格納されているPersonの属性ageの集まりから平均値を計算します。このためにValidation[Person]をValidation[Average]にしてReduce処理を行うReducerをfoldReduceメソッドに適用します。

scala> val a = persons.foldReduce(implicitly[Foldable[List]], Reducer(_.map(x => Average(x.age, 1))))
a: scalaz.Validation[NT,Average] = Success(Average(106,3))

scala> a.map(_.value)
res28: scalaz.Validation[NT,Float] = Success(35.333332)

ノート

今回の応用例は実際には以下のようにsequenceメソッドやtraverseメソッドを使う方法の方が簡単です。

sequenceメソッドを使う場合は、sequenceメソッドでList[ValidationNEL[Throwable, Person]]をValidationNEL[Throwable, List[Person]]に変換した後、List[Person]から平均年齢を求めます。

persons.sequence[VNT].map(x => x.map(_.age).sum.toFloat / x.length)

traverseメソッドを使う場合は、traverseメソッドでList[ValidationNEL[Throwable, Person]]をValidationNEL[Throwable, Int]に変換した後、List[Int]から平均年齢を求めます。

persons.traverse[VNT](_.age).map(x => x.sum.toFloat / x.length)

いずれもList[ValidationNEL[Throwable, Person]]の形をList[Person]やList[Int]というアルゴリズムを適用しやすい形にする戦略です。

そういう意味では、今回も含めReducerを使う一連の記事は日々のプログラミングで使用するイディオムとしてはすぐには活用できないかもしれません。

Reducer (5) - 演算Monoid」ではMonoidとReducerで以下の抽象化が行われることを説明しました。

ドメインオブジェクト
任意のオブジェクト(Person)
Monoid
汎用の演算+値の対を表現したモノイド(Average)
Reducer
汎用のモノイドの集まりを畳込みで1つのモノイドに集約する戦略&ドメインオブジェクトとモノイドを結びつける

今回はさらに以下の要素が加わっています。

Monad(またはApplicative)かつMonoid
汎用の計算文脈(Validation) かつMonoid。計算文脈がMonoidとして畳込み演算の対象とできる。

これらの4つの要素から構成されるメカニズムが今回のテーマというわけです。

「Monad(またはApplicative)かつMonoid」に格納された「ドメインオブジェクト」に対して、汎用演算を行う「Monoid」部品と汎用の畳込み戦略部品を結びつけるのが「Reducer」である、という構造になります。これをListなどのFoldableに対してfoldReduceメソッドで適用します。

構造が複雑なだけに、平均値の計算のような簡単な応用ではオーバースペックですが、もっと複雑な応用ではピッタリとハマるユースケースもあるのではないかと思います。

個人的には「Monad(またはApplicative)かつMonoid」としてPromiseのような並列演算のモナドを使うような応用を一つのターゲットとして考えています。この場合、sequenceメソッドやtraverseメソッドを使うと計算の開始時点で同期が発生してしまい、つまらない結果になりそうです。Promiseをそのまま維持する形で計算を進め、最後に一括して同期を行うようなことをする場合には、Reducerのようなメカニズムが有効に働くかもしれません。このあたりが今後の研究課題です。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿