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

旧城冷巷雨未停 提交于 2020-11-29 09:32:58

问题


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

回答1:


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




回答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.



来源:https://stackoverflow.com/questions/61044126/how-to-create-an-instances-for-typeclass-with-dependent-type-using-shapeless

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!