引数の型に応じて戻り型を変化させる方法といえばDependent Method Type / The Magnet Patternがあります。
例)
trait Foo[A]{
type B
def foo(a:A):B
}
object Foo{
implicit val intFoo: Foo[Int] = new Foo[Int]{
type B = String
def foo(i:Int): String = i.toString
}
implicit val stringFoo: Foo[String] = new Foo[String]{
type B = Long
def foo(i:String): Long = i.toLong
}
}
def foo[A](a:A)(implicit M:Foo[A]): M.B = M.foo(a)
利用例
scala> foo(1)
res0: Foo.intFoo.B = 1
scala> foo("123")
res1: Foo.stringFoo.B = 123
が、この方法では 型クラスを複数使うと引数同士を参照してくれない ので下記のような実装はコンパイルが通りません。
※Scala 2.x系の仕様で、implicit parameterは複数定義できません。Scala 3のDottyコンパイラでは解決されるそうです。
scala> def bar[A](a: A)(implicit M:Foo[A], N:Monoid[M.B]): M.B = N.zero
error: illegal dependent method type: parameter appears in the type of another parameter in the same section or an earlier one
def bar[A](a: A)(implicit M:Foo[A], N:Monoid[M.B]): M.B = N.zero
そこで…
Aux Pattern
下記のような定義をします。
type Aux[A0, B0] = Foo[A0] { type B = B0 }
Type aliasを使いA0をFoo Aに、B0をBに、型メンバを型パラメーターにマッピングし、暗黙の型パラメータが持つ型パラメータ同士で依存関係を表現します。
trait Foo[A] {
type B
def foo(a: A): B
}
object Foo {
type Aux[A0, B0] = Foo[A0] { type B = B0 }
implicit val intAux: Foo.Aux[Int, String] = new Foo[Int]{
type B = String
def foo(i:Int): String = i.toString
}
}
def foo[A, B](a: A)(implicit F: Foo.Aux[A, B]) = F.foo(a)
def foo[A, B](a: A)(implicit F: Foo.Aux[A, B], N:Monoid[B]): B = N.zero // 型メンバの参照ではなく、同じ型パラメータで与えているだけなのでコンパイル通る!
参考:
http://gigiigig.github.io/posts/2015/09/13/aux-pattern.html