There are several mistakes in your code.
Firstly, if you return (k1, k2)
then k1
, k2
should be the[Identifiable[K1]].identify(id._1)
, the[Identifiable[K2]].identify(id._2)
correspondingly and not vice versa as you defined them. (Typo is fixed.)
Secondly, you forgot type refinement. You declare return type of identifiableTuple
to be Identifiable[(K1,K2)]
instead of correct Identifiable[(K1,K2)] { type K = (a.K, b.K)}
(aka Identifiable.Aux[(K1,K2), (a.K, b.K)]
). If you keep Identifiable[(K1,K2)]
you actually upcast right hand side
new Identifiable[(K1,K2)]{
...
type K = (a.K, b.K)
...
}
and information that for this implicit instance type K = (a.K, b.K)
will be lost.
Since you have to restore type refinement you can't write identifiableTuple
with context bounds, you have to write it with implicit block
implicit def identifiableTuple[K1, K2](implicit
a: Identifiable[K1],
b: Identifiable[K2]
): Identifiable[(K1, K2)] {type K = (a.K, b.K)} = new Identifiable[(K1, K2)] {
type K = (a.K, b.K)
override def identify(id: (K1, K2)): K = {
val k1 = a.identify(id._1)
val k2 = b.identify(id._2)
(k1, k2)
}
}
You can test your code at compile time
implicit val int: Identifiable[Int] { type K = Double } = null
implicit val str: Identifiable[String] { type K = Char } = null
implicitly[Identifiable[(Int, String)] { type K = (Double, Char)}]
You can rewrite this with Aux
pattern type Aux[M, K0] = Identifiable[M] { type K = K0 }
implicit def identifiableTuple[K1, K2](implicit
a: Identifiable[K1],
b: Identifiable[K2]
): Identifiable.Aux[(K1, K2), (a.K, b.K)] = new Identifiable[(K1, K2)] {
type K = (a.K, b.K)
override def identify(id: (K1, K2)): K = {
val k1 = a.identify(id._1)
val k2 = b.identify(id._2)
(k1, k2)
}
} // (*)
and
implicit val int: Identifiable.Aux[Int, Double] = null
implicit val str: Identifiable.Aux[String, Char] = null
implicitly[Identifiable.Aux[(Int, String), (Double, Char)]]
This is similar to @MateuszKubuszok's answer
implicit def identifiableTuple[M1, M2, K1, K2](implicit
a: Identifiable.Aux[M1, K1],
b: Identifiable.Aux[M2, K2]
): Identifiable.Aux[(M1, M2), (K1, K2)] = new Identifiable[(M1, M2)] {
type K = (K1, K2)
override def identify(id: (M1, M2)): K = {
val k1 = a.identify(id._1)
val k2 = b.identify(id._2)
(k1, k2)
}
} // (**)
although the latter needs extra inferrence of two type parameters.
And thirdly, you can't write (*) with implicitly
or even the
inside like
implicit def identifiableTuple[K1, K2](implicit
a: Identifiable[K1],
b: Identifiable[K2]
): Identifiable.Aux[(K1, K2), (a.K, b.K)] = new Identifiable[(K1, K2)] {
type K = (a.K, b.K)
override def identify(id: (K1, K2)): K = {
val k1 = the[Identifiable[K1]].identify(id._1)
val k2 = the[Identifiable[K2]].identify(id._2)
(k1, k2)
}
}
The thing is that path-dependent types are defined in Scala so that even when a == a1
, b == b1
types a.K
and a1.K
, b.K
and b1.K
are different (a1
, b1
are the[Identifiable[K1]]
, the[Identifiable[K2]]
). So you return (k1, k2)
of wrong type (a1.K,b1.K)
.
But if you write it in (**) style
implicit def identifiableTuple[M1, M2, K1, K2](implicit
a: Identifiable.Aux[M1, K1],
b: Identifiable.Aux[M2, K2]
): Identifiable.Aux[(M1, M2), (K1, K2)] = new Identifiable[(M1, M2)] {
type K = (K1, K2)
override def identify(id: (M1, M2)): K = {
val k1 = the[Identifiable[M1]].identify(id._1)
val k2 = the[Identifiable[M2]].identify(id._2)
(k1, k2)
}
}
then it will be ok (with the
but not with implicitly
) because compiler infers that (k1,k2)
has type (K1,K2)
.