Scala summoner pattern

scalaではimplicitを利用して型クラスのインスタンスを解決するが、summoner patterというものを使えば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インスタンスが使い回されてくると、効果を発揮する。

参考

https://aakashns.github.io/better-type-class.html

ZIOへの環境Rのprovide方法各種 Scalaのシングルトン型(~.type)について 型レベル多項式の微分
View Comments
There are currently no comments.