2

I'm trying to derive a tuple instance for a type class with dependent type. I'm using shapeless to create summon the type class for the tuple elements. I'm having trouble matching tuple instance types:

import shapeless.the
import simulacrum.typeclass

@typeclass trait Identifiable[M] {
  type K
  def identify(id: M): K
}

object Identifiable{
  implicit def identifiableTuple[K1: Identifiable, K2: Identifiable]: Identifiable[(K1,K2)] = new Identifiable[(K1,K2)]{
     val b = the[Identifiable[K2]]
    val a = the[Identifiable[K1]]
    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)
        }
  }

I get this error:

type mismatch;
 found   : k1.type (with underlying type ai.fugo.cms.service.common.domain.Identifiable[K2]#K)
 required: this.a.K

type mismatch;
 found   : k2.type (with underlying type ai.fugo.cms.service.common.domain.Identifiable[K1]#K)
 required: this.b.K
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Rabzu
  • 52
  • 5
  • 26

2 Answers2

2

Try Aux pattern

trait Identifiable[M] {
  type K
  def identify(id: M): K
}

object Identifiable {
  type Aux[M, K0] = Identifiable[M] { type K = K0 }

  implicit def identifiableTuple[M1, K1, M2, K2](
    implicit
    identifiable1: Identifiable.Aux[M1, K1],
    identifiable2: Identifiable.Aux[M2, K2]
  ): Identifiable.Aux[(M1, M2), (K1, K1)] = new Identifiable[(M1, M2)] {
    type K = (K1, K2)
    def identify(id: (M1, M2)): (K1, K2) =
      identifiable1.identify(id._1) -> identifiable2.identify(id._2)
  }
}

Aux pattern was invented because

  • it is easier for human to read it
  • I think(?) compiler used to have problems with deriving type classes for path-dependent types... but not for their aliases

So just use Aux to derive things.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • Thank you, this works. There is a small typo: `Identifiable.Aux[(M1, M2), (K1, K1)]` --> `Identifiable.Aux[(M1, M2), (K1, K2)] ` – Rabzu Apr 05 '20 at 16:20
  • @MateuszKubuszok I didn't know about "problems with deriving type classes for path-dependent types... but not for their aliases". I know that version with path-dependent types `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) ...` is preferrable over version with extra type parameters since the latter needs more type parameters to be inferred. – Dmytro Mitin Apr 05 '20 at 16:51
  • 1
    I might remember things wrong (that's why I used ?) but around 2.11 if I tried to use type classes with path dependent types, the derivarion always failed. You had to use two type parameters and require implicit of type that fixed dependent type to one of them. Aux just made it simpler. Ever since I always used Aux so I am not sure if that is the case today. Perhaps I worded in badly, I'll fix it later on. – Mateusz Kubuszok Apr 05 '20 at 17:14
  • 1
    If I recall correctly compiler couldn't (cannot?) handle the idea that it runs implicit with dependent type, and returns something with dependent type. But for some reason if these dependent types are not calculated but specified explicitly as type params - then things work. I think Miles Sabin was the author of this workaround, discovered during his work on Shapeless. But ATM I cannot check this so take it with a grain of salt. – Mateusz Kubuszok Apr 05 '20 at 17:26
  • @MateuszKubuszok I found even in Scala 2.13 and Scala 3 cases where implicits are found if an extra type parameter is used and not found if a path-dependent type is used https://github.com/lampepfl/dotty/issues/17212 https://github.com/scala/bug/issues/12767 – Dmytro Mitin Apr 06 '23 at 14:36
2

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).

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thank you for a detailed explanation – Rabzu Apr 05 '20 at 17:10
  • What do you mean by "If you keep Identifiable[(K1,K2)] you actually upcast right hand side"? – Rabzu Apr 05 '20 at 17:15
  • @Rabzu If you have `class Parent` `class Child` `val child: Parent = new Child` you statically (i.e. at compile time) upcast value `new Child` of type `Child` till type `Parent`. Right hand side of `identifiableTuple` has type `Identifiable[(K1, K2)]{type K = (a.K, b.K)}`. So when you write that its return type is just `Identifiable[(K1, K2)]` you actually upcast right hand side because `implicitly[Identifiable[(K1, K2)]{type K = (a.K, b.K)} <:< Identifiable[(K1, K2)]]`. – Dmytro Mitin Apr 05 '20 at 17:25
  • Can I use the above type class like this? `def a[K: Identifiable](key:K) = { case k => val keyString: String = the[Identifiable.Aux[K, String]].identify(key) case (k1,k2) => val keyString: (String,String) = the[Identifiable.Aux[K, (String, String)]].identify(key) }` – Rabzu Apr 05 '20 at 17:34
  • @Rabzu I don't think so (most probably). You should better start a new question. – Dmytro Mitin Apr 05 '20 at 17:39