任意のリストの合計を計算する sum メソッドを作ったときには、「足すことのできる」型クラスとして Additive という型クラスと、そのインスタンス IntAdditive、StringAdditive を作った。
今回は、リストの平均を求める average メソッドを作ってみよう。Int に限れば次のように定義することができる。要素を合計して要素数で割っているだけだ。
scala> def average(lst: List[Int]): Int = lst.foldLeft(0)((x, y) => x + y) / lst.length average: (lst: List[Int])Int
使い方も簡単。
scala> average(List(1, 3, 5)) res0: Int = 3
さて、この average メソッドを、Int でも Double でも使えるようにしたい。
合計を求めるのには Additive を使えばいいだろう。だけどそれだけじゃ足りない。1つには、要素数で割る必要があるので割り算もできなければならない。もう1つは、要素数を求める length メソッドは Int を返すので、これを合計の型(Int か Double)に合わせてやる必要がある。
そこで、Additive をもっと一般化して、四則演算とゼロを持つ Num という型クラスを考えよう。で、Int と Double にそれぞれ対応する IntNum と DoubleNum というインスタンスを作る。ファイルは Num.scala。
trait Num[A] { def plus(a: A, b: A): A def minus(a: A, b: A): A def multiply(a: A, b: A): A def divide(a: A, b: A): A def zero: A } object Num { implicit object IntNum extends Num[Int] { def plus(a: Int, b: Int): Int = a + b def minus(a: Int, b: Int): Int = a - b def multiply(a: Int, b: Int): Int = a * b def divide(a: Int, b: Int): Int = a / b def zero: Int = 0 } implicit object DoubleNum extends Num[Double] { def plus(a: Double, b: Double): Double = a + b def minus(a: Double, b: Double): Double = a - b def multiply(a: Double, b: Double): Double = a * b def divide(a: Double, b: Double): Double = a / b def zero: Double = 0.0 } }
それから、Int から変換する FromInt という型クラスと、FromIntToInt、FromIntToDoubleというインスタンスを作る。ファイルは FromInt.scala。
trait FromInt[A] { def to(from: Int): A } object FromInt { implicit object FromIntToInt extends FromInt[Int] { def to(from: Int): Int = from } implicit object FromIntToDouble extends FromInt[Double] { def to(from: Int): Double = from } }
sbt console を起動して2つのファイルを読み込めば、準備は完了。
average メソッドの定義は次のようになる。
scala> def average[A](lst: List[A])(implicit a: Num[A], b: FromInt[A]): A = { | val length: Int = lst.length | val sum: A = lst.foldLeft(a.zero)((x, y) => a.plus(x, y)) | a.divide(sum, b.to(length)) | } average: [A](lst: List[A])(implicit a: Num[A], implicit b: FromInt[A])A
ここで1つ勘違いをしていたのを白状しよう。「implicit キーワードは引数リストの先頭にしか付けられない」というので、てっきり先頭の引数だけが implicit になるのだと思っていた。実際には引数リスト全体が implicit になるんだね。というわけで、ここでは a と b が implicit parameter になっている。言い換えると Num と FromInt という2つの型クラスを使っている。
さあ、最後に試してみよう。
scala> average(List(1, 3, 5)) res0: Int = 3 scala> average(List(1.5, 2.5, 3.5)) res1: Double = 2.5
このとおり、Int にも Double にも対応した average メソッドができた。