How to create an instances for typeclass with dependent type using shapeless

Viewed 179

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
2 Answers

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.

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

Related