Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
DmytroMitin opened this issue Apr 5, 2023 · 1 comment
Labels
area:implicits related to implicits itype:bug

Comments

@DmytroMitin
Copy link

DmytroMitin commented Apr 5, 2023

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) scala/bug#12767

@DmytroMitin DmytroMitin added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Apr 5, 2023
@DmytroMitin
Copy link
Author

DmytroMitin commented Apr 5, 2023

Just for the context, here is more meaningful example. Repo https://github.com/DmytroMitin/phantom, branch hlisteqs, commit baf6301344245025af185186eab56ae9cada2e63 outworkers/phantom#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 scala/bug#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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:implicits related to implicits itype:bug
Projects
None yet
Development

No branches or pull requests

2 participants