2012年7月3日火曜日

Scala Tips / Generator

Reducerの使い方の一つとして、FoldableのfoldReduceメソッドを使う方法があります。ListやStreamといったSeqもFoldableですから、一般的にはSeqのfoldReduceメソッドを使う方法と考えてもよいでしょう。

foldReduceメソッドは手軽で良いのですが、あえて言えば畳込み戦略が固定化(通常は右畳込み戦略)されているのが懸念事項です。

そこで、畳込み戦略を選択するためのメカニズムとして用意されているのがGeneratorです。GeneratorはReducer向けに要素の集まりと畳込み戦略をカプセル化したオブジェクトです。Reducerを使って畳込みを行います。

Reducerは任意のオブジェクトを任意のMonoidと結びつけて、畳込み処理を行うための抽象といえます。また、GeneratorはReducerを任意の畳込み戦略に結びつけて畳込み処理を行うための抽象といえます。つまり、ReducerとGeneratorを併用することで、ドメインオブジェクト、Monoidの汎用部品、畳込み戦略の汎用部品を適材適所で組合せて使用することができるようになります。

今回はこのGeneratorの使い勝手を試して見ることにします。

課題は「Reducer (4) - 自前Reducer」、「Reducer (5) - 演算Monoid」と同じもの使うことにします。これにGeneratorを適用します。

課題

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))

準備

Reducer (5) - 演算Monoid」では、MonoidとしてAverageを、ReducerとしてPersonAgeAverageReducerを使用しました。それぞれ以下になります。

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
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
}

Generator

Generatorは畳込み戦略の抽象で、汎用の畳込み戦略の部品を用意できるメカニズムを備えていますが、現状で用意されている部品は以下の3つです。

Generator#FoldrGenerator
Foldableに対する右畳込み
Generator#FoldlGenerator
Foldableに対する左畳込み
Generator#FoldMapGenerator
Foldableに対するfoldMap畳込み

ここではFoldrGeneratorを使うことにします。

FoldrGeneratorを取得したあと、reduceメソッドにReducerとしてPersonAgeAverageReducerを、オブジェクトの集まりとしてPersonのListを指定すると以下に示すように平均を示すAverageオブジェクトが返されます。

scala> val g = Generator.FoldrGenerator[List]
g: scalaz.Generator[List] = scalaz.Generator$$anon$1@5fdde23d

scala> val a = g.reduce(PersonAgeAverageReducer, persons)
a: Average = Average(106,3)

scala> a.value
res27: Float = 35.333332

FoldrGeneratorの取得とreduceメソッドによる演算を一行にまとめると以下になります。

scala> Generator.FoldrGenerator[List].reduce(PersonAgeAverageReducer, persons)
res26: Average = Average(106,3)

scala> Generator.FoldrGenerator[List].reduce(PersonAgeAverageReducer, persons).value
res27: Float = 35.333332

ノート

Reducer (5) - 演算Monoid」で説明したようにfoldReduceメソッドの使い方は大きく以下の2つがあります。

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

前者はimplicitlyを使うのが煩雑です。後者はわざわざ暗黙パラメタを使うのが大げさな感じがしますし、プログラムの見通しも悪くなりそうです。

そういう意味で、Generatorの使い勝手がよければGeneratorを使うのを基本戦略にしてもよいのですが、Generatorが際立って簡潔に書けるわけでもありません。

また、事前に定義されているGeneratorは、前述したようにFoldableの基本機能を使用する3つだけなので、豊富な汎用部品が用意されているという状況でもありません。

このため、状況に応じて好みで選択することになりますが、ボクの場合はfoldReduceメソッドとimplicitlyを使う方式を選ぶことが多くなりそうです。

畳込み戦略の選択

foldReduceメソッドでは、第一引数に指定するFoldableによって畳込み戦略が決まります。具体的にはFoldableのfoldMapメソッドの畳込み戦略が使用されます。Foldableのデフォルトの実装ではfoldMapは右畳込みを使用するのようなっていて、ListやSeqの場合に使われる型クラスインスタンスTraversableFoldableもこれを踏襲しています。つまり、foldReduceメソッドは概ね右畳込みを行うと考えておいてよいでしょう。

foldの選択」で説明した通りで普通の使い方では、右畳込み戦略が最良の選択でネガティブインパクトもほとんどないので、デフォルトの戦略としては妥当です。このため、普通はReducerはfoldReduceメソッドと組合せて使うと考えておけば十分でしょう。

複数の畳込み戦略の中から、状況に応じて畳込み戦略を切り替えるようなケースで、Generatorを用いる価値が出てきます。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿