アドホック多相の具体例として、Int
のリストを合計する簡単な関数 sum
を徐々に一般化していく。
def sum(xs: List[Int]): Int = xs.foldLeft(0) { _ + _ }
sum(List(1, 2, 3, 4))
// res0: Int = 10
これを少し一般化してみましょう。
Monoid
というものを取り出します。… これは、同じ型の値を生成するmappend
という関数と「ゼロ」を生成する関数を含む型です。
object IntMonoid {
def mappend(a: Int, b: Int): Int = a + b
def mzero: Int = 0
}
これを代入することで、少し一般化されました。
def sum(xs: List[Int]): Int = xs.foldLeft(IntMonoid.mzero)(IntMonoid.mappend)
sum(List(1, 2, 3, 4))
// res2: Int = 10
次に、全ての型
A
についてMonoid
が定義できるように、Monoid
を抽象化します。これでIntMonoid
がInt
のモノイドになりました。
trait Monoid[A] {
def mappend(a1: A, a2: A): A
def mzero: A
}
object IntMonoid extends Monoid[Int] {
def mappend(a: Int, b: Int): Int = a + b
def mzero: Int = 0
}
これで sum
が Int
のリストと Int
のモノイドを受け取って合計を計算できるようになった:
def sum(xs: List[Int], m: Monoid[Int]): Int = xs.foldLeft(m.mzero)(m.mappend)
sum(List(1, 2, 3, 4), IntMonoid)
// res4: Int = 10
これで
Int
を使わなくなったので、全てのInt
を一般型に置き換えることができます。
def sum[A](xs: List[A], m: Monoid[A]): A = xs.foldLeft(m.mzero)(m.mappend)
sum(List(1, 2, 3, 4), IntMonoid)
// res6: Int = 10
最後の変更点は
Monoid
を implicit にすることで毎回渡さなくてもいいようにすることです。
def sum[A](xs: List[A])(implicit m: Monoid[A]): A = xs.foldLeft(m.mzero)(m.mappend)
{
implicit val intMonoid = IntMonoid
sum(List(1, 2, 3, 4))
}
// res8: Int = 10
Nick さんはやらなかったけど、この形の暗黙のパラメータは context bound で書かれることが多い:
def sum[A: Monoid](xs: List[A]): A = {
val m = implicitly[Monoid[A]]
xs.foldLeft(m.mzero)(m.mappend)
}
{
implicit val intMonoid = IntMonoid
sum(List(1, 2, 3, 4))
}
// res10: Int = 10
これでどのモノイドのリストでも合計できるようになり、
sum
関数はかなり一般化されました。String
のMonoid
を書くことでこれをテストすることができます。また、これらはMonoid
という名前のオブジェクトに包むことにします。その理由は Scala の implicit 解決ルールです。ある型の暗黙のパラメータを探すとき、Scala はスコープ内を探しますが、それには探している型のコンパニオンオブジェクトも含まれるのです。
trait Monoid[A] {
def mappend(a1: A, a2: A): A
def mzero: A
}
object Monoid {
implicit val IntMonoid: Monoid[Int] = new Monoid[Int] {
def mappend(a: Int, b: Int): Int = a + b
def mzero: Int = 0
}
implicit val StringMonoid: Monoid[String] = new Monoid[String] {
def mappend(a: String, b: String): String = a + b
def mzero: String = ""
}
}
def sum[A: Monoid](xs: List[A]): A = {
val m = implicitly[Monoid[A]]
xs.foldLeft(m.mzero)(m.mappend)
}
sum(List("a", "b", "c"))
// res12: String = "abc"
この関数に直接異なるモノイドを渡すこともできます。例えば、
Int
の積算のモノイドのインスタンスを提供してみましょう。
val multiMonoid: Monoid[Int] = new Monoid[Int] {
def mappend(a: Int, b: Int): Int = a * b
def mzero: Int = 1
}
// multiMonoid: Monoid[Int] = repl.MdocSession3@6082c022
sum(List(1, 2, 3, 4))(multiMonoid)
// res13: Int = 24