モノイドは関数型プログラミングで非常に有効な性質です。この性質をプログラムの機能として使えるようにしたものがScalazの型クラスMonoidです。
Scalazが定義する様々なMonoidを活用するのはもちろんですが、アプリケーションのドメインで有用な新たなMonoidを定義して追加していくのもScalaプログラミングの重要な構成要素です。
今回は平均値を示すクラスAverageをMonoid化してみましょう。
Average
まず平均値を示すクラス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 }
Averageの動きは以下のとおりです。
scala> val a = Average(0, 0) :+ 3 a: Average = Average(3,1) scala> a.value res35: Float = 3.0 scala> val b = a :+ 1 b: Average = Average(4,2) scala> b.value res36: Float = 2.0 scala> val c = a + b c: Average = Average(7,3) scala> c.value res37: Float = 2.3333333
Monoid
「Scalazの型クラス」のMonoidの項を見ると、型クラスMonoidは型クラスZeroと型クラスSemigroupを継承しています。これは「モノイド」にある"モノイドは単位元をもつ半群(単位的半群)である"という説明と符合するので面白いですね。
ScalazでクラスをMonoid化するには、対象となるクラスに対応する型クラスZeroの型クラスインスタンスと型クラスSemigroupの型クラスインスタンスを定義します。
Scalazの場合は、以下のように型クラスZeroのインスタンスを返す暗黙変換関数と型クラスSemigroupのインスタンスを返す暗黙変換関数を定義します。さらに、暗黙変換をアプリケーションに取り込むためのメカニズムとして、トレイトとオブジェクトの両方を用意します。
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
型クラスMonoid
Monoidを定義する際に、型クラスZeroと型クラスSemigroupのインスタンスを返す暗黙変換関数を定義する方法とは別に、型クラス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 AverageMonoid: Monoid[Average] = new Monoid[Average] { val zero = Average(0, 0) def append(x: Average, y: => Average) = x + y } } object Averages extends Averages
利用方法
まずimport文でAverageの型クラスインスタンスを有効にします。
scala> import Averages._ import Averages._
mzero関数で単位元を取得することができます。
scala> mzero[Average] res29: Average = Average(0,0)
演算子|+|で、加算的なモノイド演算を行うことができます。
scala> val a = Average(10, 1) |+| Average(20, 2) a: Average = Average(30,3) scala> a.value res32: Float = 10.0
AverageはMonoidになったので、Monoidに対する便利機能を使うことができます。たとえばListのsumrメソッドを使って、Averageの集約を行うことができます。
scala> val b = List(Average(10, 1), Average(20, 2), Average(30, 3)).sumr b: Average = Average(60,6) scala> b.value res34: Float = 10.0
ノート
Scalazのバージョンは安定版のScalaz 6と新バージョンで開発中のScalaz 7があります。本ブログではボクが開発に使用しているScalaz 6を題材にしていますが、基本的にはScalaz 7でもそれほど影響がなさそうな基本的な項目を中心に調査を進めています。
ただ、今回のテーマである型クラスの新規作成は、Scalaz 6とScalaz 7で仕組みが変わりそうなところなので注意が必要です。Scalaz 7を開発に使うようになったら、Scalaz 7版の定義方法をまとめる予定です。
諸元
- Scala 2.9.2
- Scalaz 6.0.4