0

I'm trying to use a type-dependent type class in a method as below:

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

object Identifiable {
  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)
    }
  }

However, when I try to use it like bellow the implicit parameters is not resolved

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

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Rabzu
  • 52
  • 5
  • 26
  • But why would you expect them to match? In `case k =>` branch, there is an implicit of type `Identifiable[K]` but there is no reason to expect it to be an `Identifiable[K] { type K = String }` which you try to ask for with `the`. Or an `Identifiable[K] { type K = (String, String) }` in the other branch. – Alexey Romanov Apr 05 '20 at 18:12
  • 1
    I have seen a couple of questions from you recently, and my impression is that every answer you get is only confusing you even more. I would re-start by the beginning, why do not you open a new question telling us what are you trying to model and showing us this code as what you have tried, maybe the solution you need may be simpler that what you expect. – Luis Miguel Mejía Suárez Apr 05 '20 at 18:16

3 Answers3

2

You demand implicit with path-dependent type in type constraint but then you summon implicit with that type fixed to something.

For that to work compiler would have to be able to prove that dependent type in Identifiable is equal to K which in current code it cannot do.

You can try:

  • use the same convention in both places (either both with Aux or both with dependent type)
  • prove that the dependent type is equal to K with implicit evidence ev: identifiable.K =:= K
Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • "use the same convention in both places (either both with Aux or both with dependent type) " `def a[K](key:K) (implicit identifiable: Identifiable.Aux[K,?])` get stuck on ? – Rabzu Apr 05 '20 at 17:58
  • Either `[M, K](implicit identifiable: Identifiable.Aux[M, K])` and `the[Identifiable.Aux[M, K]` or `[M: Identifiable]` and them summon `the[Identifiable[M]]`. The former requires you to specify 2 type params but make it explicit that you need key of type `K`, the later requires you to specify 1 type param but then you cannot rely on more knowledge about key than that it is of `identifiable.K` type. – Mateusz Kubuszok Apr 05 '20 at 19:03
1

Pattern (k1,k2) should be before unconstrained pattern k, otherwise everything will go to k and nothing to (k1,k2).

Pattern matching mostly works at runtime and type classes/implicits mostly work at compile time. Since you started to work with type classes (and moreover with type classes with type member) it looks like you want to have some calculations on types i.e. some calculations at compile time.

Logic branching at runtime corresponds to pattern matching. Logic branching at compile time corresponds to a type class and several implicits defining instances of the type class.

Try to make a() a type class

trait A[K] {
  def apply(key: K): Unit
}

object A {
  implicit def tupleA[K](implicit identifiable: Identifiable.Aux[K, (String, String)]): A[K] = new A[K] {
    override def apply(key: K): Unit = key match {
      case (k1, k2) =>
        val keyString: (String, String) = identifiable.identify(key)
        ()
    }
  }

  implicit def defaultA[K](implicit identifiable: Identifiable.Aux[K, String]): A[K] = new A[K] {
    override def apply(key: K): Unit = {
      val keyString: String = identifiable.identify(key)
      ()
    }
  }
}

def a[K](key: K)(implicit aInstance: A[K]) = aInstance(key)

You should write some examples how you're planning to apply your a(). On what inputs do you expect what outputs. What should compile, what should not compile?

Normally type parameters (like M for Identifiable[M]) are like inputs and type members (like K for Identifiable[M] { type K }) are like outputs for type-level calculations. So for Identifiable it looks like type K will be chosen basen on what type M is. Is it correct? Please think about your logic (what are inputs, what outputs). In a() you seem start doing something based on what type K is.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • case class K1(value: String); case class K2(value: String) I expect: a(K1) => String and a((K1,K2)) => (String, String) – Rabzu Apr 05 '20 at 18:13
  • @Rabzu If you want `a` to return different types based on type of input then type class `A` also should have "return" type member `trait A[K] { type Out }`. – Dmytro Mitin Apr 05 '20 at 18:17
1

But why would you expect them to match? In case k => branch, there is an implicit of type Identifiable[K] but there is no reason to expect it to be an Identifiable[K] { type K = String } which you try to ask for with the. Or an Identifiable[K] { type K = (String, String) } in the other branch. (Note that you have two different meanings for K which may be a bit confusing.)

[From a comment] case class K1(value: String); case class K2(value: String) I expect: a(K1) => String and a((K1,K2)) => (String, String)

The most reasonable option close to what you want seems to be simply

def a[A](key:A)(implicit ev: Identifiable[A]): ev.K = ev.identify(key)
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487