Scala summoner pattern
scalaではimplicitを利用して型クラスのインスタンスを解決するが、summoner patternというものを使えばimplicitを愚直に書くより少し楽になるよというお話。
まずは普通に
combineという二項演算のみできる型クラスSemigroupを考える。
trait Semigroup[T] {
def combine(x: T, y: T): T
}
// Int用インスタンス
implicit def intSemi: Semigroup[Int] = (x: Int, y: Int) => x + y
// なんか普通のメソッドなのにimplicitのせいで長く見えない?
def plus[T](x: T, y: T)(implicit semigroup: Semigroup[T]): T = semigroup.combine(x, y)
plus(1, 2) // => 3
まあよく見るやつですね。
これのimplicit parameterを書くのがめんどくさいなぁ。
Context Bound
上の課題はcontext boundという機能を使えば解決できる。
trait Semigroup[T] {
def combine(x: T, y: T): T
}
implicit def intSemi: Semigroup[Int] = (x: Int, y: Int) => x + y
// TはSemigroup型のインスタンスを持つことが保証される
def plus[T: Semigroup](x: T, y: T): T = implicitly[Semigroup[T]].combine(x, y)
plus(1, 2) // => 3
plusのシグネチャで書いているT: Semigroup
というのがcontext boundでimplicitパラメタのシンタックスシュガーである。
使うための条件はTがその後ろの型の型パラメタとして入る、つまりT: F = implicit f: F[T]
である。
だからEitherみたいなジェネリクスが2つある型コンストラクタには使えない。
あとはSemigroup[T]が変数に束縛されなくなってしまったので、それをimplicitlyで取り出せばいい。
でもこのimplicitlyも書くのめんどくさいなぁ。
Summoner Pattern
summoner patternを使った最終形は以下。
trait Semigroup[T] {
def combine(x: T, y: T): T
}
object Semigroup {
// summoner method
def apply[T: Semigroup]: Semigroup[T] = implicitly[Semigroup[T]]
}
implicit def intSemi: Semigroup[Int] = (x: Int, y: Int) => x + y
def plus[T: Semigroup](x: T, y: T): T = Semigroup[T].combine(x, y)
plus(1, 2) // => 3
簡単に言えば型クラスのコンパニオンオブジェクトにsummoner methodと呼ばれるものを定義すればいいのだ。
御存知の通りscalaではapplyメソッドは特別な扱いを受ける。
すなわち上の例でSemigroup[T] = Semigroup.apply[T]
はimplicitly[Semigroup[T]]
を呼び寄せる。
あとはContext Boundのときと同じ。
上の例だとただimplicitの書く場所変わっただけじゃないかという感じだが、いろんなとこでSemigroupインスタンスが使い回されてくると、効果を発揮する。