2012年7月2日月曜日

Scala Tips / Reducer (5) - 演算Monoid

Monoid - 新規作成」では平均値を表現するモノイドAverageをScalazのMonoidとして作成しました。

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

このAverage MonoidとReducerを組み合わせてモノイドではないオブジェクトPersonの平均年齢を求める処理を考えます。

ドメイン・オブジェクトと用途毎のモノイドをReducerで組み合わせるという使い方です。

課題は「Reducer (4) - 自前Reducer」と同じものです。これを専用Reducerを使って実現します。

課題

Personの集まりから平均年齢を計算します。ケースクラスPersonは以下のものとします。

case class Person(name: String, age: Int)

Personの集まりを以下の通り定義します。

scala> val taro = new Person("Taro", 35)
taro: Person = Person(Taro,35)

scala> val hanako = new Person("Hanako", 28)
hanako: Person = Person(Hanako,28)

scala> val saburo = new Person("Saburo", 43)
saburo: Person = Person(Saburo,43)

scala> val persons = List(taro, hanako, saburo)
persons: List[Person] = List(Person(Taro,35), Person(Hanako,28), Person(Saburo,43))

PersonAgeAverageReducer

ドメイン・オブジェクトであるPersonと、汎用的なモノイドであるAverageを、モノイド演算による畳込み演算という切り口で結びつけるReducerとしてPersonAgeAverageReducerを作ります。

object PersonAgeAverageReducer extends Reducer[Person, Average] {
  override def unit(c: Person) = Average(c.age, 1)
  override def cons(c: Person, m: Average) = c.age +: m
  override def snoc(m: Average, c: Person) = m :+ c.age
}

Reducerは、「あるクラス」と「あるクラス」に対する別モノイド演算を定義した「別のクラス」を結びつける演算を行うオブジェクトです。元のクラスである「あるクラス」をC、「追加のモノイド演算」と「あるクラス」を結びつける「別のクラス」をMと表記することにします。

PersonAgeAverageReducerは、CがPerson、MがAverageになります。MonoidではないクラスPersonに対して、平均値の計算のための値とモノイド演算の対をパッケージングしたMonoidであるオブジェクトAverageを結びつけます。

使ってみる

それではPersonAgeAverageReducerを使ってPersonの集まりの平均年齢を計算してみましょう。

scala> val avg = persons.foldReduce(implicitly[Foldable[List]], PersonAgeAverageReducer)
avg: Average = Average(106,3)

scala> avg.value
res18: Float = 35.333332

Reducerを暗黙パラメタにすることで、foldReduceでimplicitlyの指定を減らすことができます。

scala> val avg = {
     | implicit val r = PersonAgeAverageReducer
     | ps.foldReduce
     | }
avg: Average = Average(106,3)

scala> avg.value
res19: Float = 35.333332

暗黙パラメタはプログラムの表層からは見えなくなるので予想外のバグを引き起こす危険性もあり使い方には注意が必要ですが、うまくハマるとかなり便利です。

適材適所で使い分けるとよいでしょう。

このケースの場合には、個人的には冗長でもimplicitlyを使って、暗黙パラメタを2つとも陽に指定する方式の方が良いかなと思います。

ノート

今回の課題は、以下のように普通にやっても簡単にかけるので、わざわざMonoidやReducerを持ち出す必要はありません。

scala> ps.map(_.age.toFloat).sum / ps.length
res21: Float = 35.333332

説明のために簡単な処理を課題として用いているからですが、もうちょっと込み入ったロジックであれば、使い所がありそうな予感がします。

お互いに疎の関係にある以下の2つのオブジェクトがあります。

  • 任意のオブジェクト(Person)
  • 任意の演算+値の対を表現したモノイド(Average)

また、以下の頻出の汎用的な演算があります。

  • モノイドの集まりを畳込みで1つのモノイドに集約する

この3つの要素を一つにまとめるためのオブジェクトがReducerということになります。

別の言い方をすると「モノイドの集まりを畳込みで1つのモノイドに集約する」をターゲットに「任意のオブジェクト」の"ある値"を「モノイド」に結びつけるのがReducerということです。「モノイド」として今回は汎用部品である平均値を考えましたが、他にも中央値や標準偏差といった部品も考えられます。

汎用部品としてのモノイドが整備されてくれば、これをアプリケーションのドメイン・オブジェクトと結びつけてロジックを組みたいケースも増えてくるでしょう。そういった状況になればReducerが活用できる場も増えてくるかもしれません。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿