scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.84k stars 1.05k forks source link

Can't find implicit delegating to lower-priority implicit via path-dependent type #17212

Open DmytroMitin opened 1 year ago

DmytroMitin commented 1 year ago

Compiler version

3.3.0-RC3

Motivation

I have two type classes. They are working on type level. They accept a type and return a type. The 1st type class delegates to the 2nd (it can do some extra work but for minimality it just delegates). The 2nd type class returns Int for any type T (higher-priority instance) or String for any type T (lower-priority instance). In a more meaningful example the "return type" is calculated in some way and the higher-priority instance manages the case with additional upper bound or additional type-class constraint but for minimality it's just two similar instances, higher-priority and lower-priority. I make the "return type" of the 1st type class a type member.

  1. If I make the "return type" of the 2nd type class a type parameter then everything compiles
trait TC1[T]:
  type S
object TC1:
  type Aux[T, S0] = TC1[T] {type S = S0}
  given [T, S](using TC2[T, S]): Aux[T, S] = null

trait TC2[T, S]
trait LowPriorityTC2:
  given [T]: TC2[T, String] = null
object TC2 extends LowPriorityTC2:
  given [T]: TC2[T, Int] = null

summon[TC1.Aux[Boolean, String]] // compiles
summon[TC2[Boolean, String]]     // compiles
summon[TC1.Aux[Boolean, Int]]    // compiles
summon[TC2[Boolean, Int]]        // compiles

https://scastie.scala-lang.org/DmytroMitin/MEyLY8TkRfaif5C778wkRw/1

  1. If I make the "return type" of the 2nd type class a type member and use an extra type parameter in the instance declaration of the 1st type class then everything compiles too
trait TC1[T]:
  type S
object TC1:
  type Aux[T, S0] = TC1[T] { type S = S0}
  given [T, S](using TC2.Aux[T, S]): Aux[T, S] = null

trait TC2[T]:
  type S
trait LowPriorityTC2:
  type Aux[T, S0] = TC2[T] {type S = S0}
  given [T]: Aux[T, String] = null
object TC2 extends LowPriorityTC2:
  given [T]: Aux[T, Int] = null

summon[TC1.Aux[Boolean, String]] // compiles
summon[TC2.Aux[Boolean, String]] // compiles
summon[TC1.Aux[Boolean, Int]]    // compiles
summon[TC2.Aux[Boolean, Int]]    // compiles

https://scastie.scala-lang.org/DmytroMitin/MEyLY8TkRfaif5C778wkRw/2

  1. But if I make the "return type" of the 2nd type class a type member and do not use an extra type parameter in the instance declaration of the 1st type class, using path-dependent type instead, then the code doesn't compile (even although the code manually resolved compiles):

Minimized code

trait TC1[T]:
  type S
object TC1:
  type Aux[T, S0] = TC1[T] { type S = S0}
  given [T](using tc2: TC2[T]): Aux[T, tc2.S] = null

trait TC2[T]:
  type S
trait LowPriorityTC2:
  type Aux[T, S0] = TC2[T] {type S = S0}
  given [T]: Aux[T, String] = null
object TC2 extends LowPriorityTC2:
  given [T]: Aux[T, Int] = null

summon[TC1.Aux[Boolean, String]] // doesn't compile
summon[TC1.Aux[Boolean, String]](using TC1.given_Aux_T_S[Boolean](using TC2.given_Aux_T_String[Boolean])) // compiles
summon[TC2.Aux[Boolean, String]] // compiles
summon[TC1.Aux[Boolean, Int]]    // compiles
summon[TC2.Aux[Boolean, Int]]    // compiles

https://scastie.scala-lang.org/DmytroMitin/MEyLY8TkRfaif5C778wkRw

Output

No given instance of type TC1.Aux[Boolean, String] was found for parameter x of method summon in object Predef.
I found:
    TC1.given_Aux_T_S[Boolean](TC2.given_Aux_T_Int[Boolean])
But given instance given_Aux_T_S in object TC1 does not match type TC1.Aux[Boolean, String].
  summon[TC1.Aux[Boolean, String]]

Expectation

summon[TC1.Aux[Boolean, String]] in 3) should compile too.

Or is it intended difference between type parameters and type members? (Functional dependencies?)

In Scala 2.13 behavior is the same

  1. https://scastie.scala-lang.org/DmytroMitin/mKmrLm71SPiOuXLBpx1ulg
  2. https://scastie.scala-lang.org/DmytroMitin/mKmrLm71SPiOuXLBpx1ulg/1
  3. https://scastie.scala-lang.org/DmytroMitin/mKmrLm71SPiOuXLBpx1ulg/2

But in 2.13 there is easier reproduction (without lower priority) https://github.com/scala/bug/issues/12767

DmytroMitin commented 1 year ago

Just for the context, here is more meaningful example. Repo https://github.com/DmytroMitin/phantom, branch hlisteqs, commit baf6301344245025af185186eab56ae9cada2e63 https://github.com/outworkers/phantom/pull/934

I define instances of a type class

trait SingleGeneric[T, Store, GenR] {
  type Repr = Store
  def to(t: T): Repr
  def from(r: Repr): T
}
trait LowPrioritySingleGeneric {
  implicit def single[T, HL](implicit gen: shapeless.Generic.Aux[T, HL]): SingleGeneric[T, T :: HNil, HL] =
    new SingleGeneric[T, T :: HNil, HL] {
      def to(source: T): T :: HNil = source :: HNil
      def from(hl: T :: HNil): T = hl.head
    }
}
object SingleGeneric extends LowPrioritySingleGeneric {
  implicit def generic[T, HL](implicit gen: shapeless.Generic.Aux[T, HL]): SingleGeneric[T, HL, HL] =
    new SingleGeneric[T, HL, HL] {
      def to(source: T): HL = gen to source
      def from(hl: HL): T = gen from hl
    }
}

but if I remove the extra type parameter in instance declarations and use path-dependent type instead then the project will not compile (Scala 2.13)

trait LowPrioritySingleGeneric {
  implicit def single[T](implicit gen: Generic[T]): SingleGeneric[T, T :: HNil, gen.Repr] =
    new SingleGeneric[T, T :: HNil, gen.Repr] {
      def to(source: T): T :: HNil = source :: HNil
      def from(hl: T :: HNil): T = hl.head
    }
}
object SingleGeneric extends LowPrioritySingleGeneric {
  implicit def generic[T](implicit gen: Generic[T]): SingleGeneric[T, gen.Repr, gen.Repr] =
    new SingleGeneric[T, gen.Repr, gen.Repr] {
      def to(source: T): gen.Repr = gen to source
      def from(hl: gen.Repr): T = gen from hl
    }
}

Here TC1 is SingleGeneric, TC2 is Generic. This is kind of code snippet 6 in https://github.com/scala/bug/issues/12767 . In Scala 3 this compiles: https://scastie.scala-lang.org/DmytroMitin/C8ziePLmSfifN2Je2Wsbcw/1


If I split the type class into two then SingleGeneric1 and SingleGeneric2 will be TC1 and TC2

package object macros {
  type SingleGeneric[T, Store, GenR] = SingleGeneric1.Aux[T, Store, GenR]
}

trait SingleGeneric1[T] extends Serializable {
  type Store
  type GenR
  type Repr = Store
  def to(t: T): Store
  def from(s: Store): T
}
object SingleGeneric1 {
  type Aux[T, S, R] = SingleGeneric1[T] {type Store = S; type GenR = R}
  def instance[T, S, R](f: T => S, g: S => T): Aux[T, S, R] = new SingleGeneric1[T] {
    override type Store = S
    override type GenR = R
    override def to(t: T): Store = f(t)
    override def from(s: Store): T = g(s)
  }

    //  the project compiles
  implicit def mkSingleGeneric1[T, S](implicit
    sgen2: SingleGeneric2.Aux[T, S],
    gen: Generic[T]
  ): Aux[T, S, gen.Repr] = instance(sgen2.to(_), sgen2.from)

    //  the project doesn't compile
  //implicit def mkSingleGeneric1[T](implicit
  //  sgen2: SingleGeneric2[T],
  //  gen: Generic[T]
  //): Aux[T, sgen2.Store, gen.Repr] = instance(sgen2.to(_), sgen2.from)
}

trait SingleGeneric2[T] extends Serializable {
  type Store
  def to(t: T): Store
  def from(s: Store): T
}
trait LowPrioritySingleGeneric2 {
  type Aux[T, S] = SingleGeneric2[T] {type Store = S}
  def instance[T, S](f: T => S, g: S => T): Aux[T, S] = new SingleGeneric2[T] {
    override type Store = S
    override def to(t: T): Store = f(t)
    override def from(s: Store): T = g(s)
  }

  implicit def single[T]: Aux[T, T :: HNil] = instance(_ :: HNil, _.head)
}
object SingleGeneric2 extends LowPrioritySingleGeneric2 {
  implicit def generic[T](implicit gen: Generic[T]): Aux[T, gen.Repr] =
    instance(gen.to(_), gen.from)
}

In Scala 3 this doesn't compile either: https://scastie.scala-lang.org/DmytroMitin/C8ziePLmSfifN2Je2Wsbcw/6 This is kind of item 3 in the current issue.