2つの型クラスを使う

任意のリストの合計を計算する 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 メソッドができた。