1

Consider the following snippet:

object Test extends App {
  class X {
    class Y
  }

  class Z(val x: X) {
    val y: x.Y = new x.Y
  }

  val x: X = new X
  val z: Z = new Z(x)
  val y: x.Y = z.y

  println(y)
}

This code won't compile, complaing about incompatible path-dependent types:

[error] 12 |  val y: x.Y = z.y
[error]    |               ^^^
[error]    |               Found:    (Test.z.y : Test.z.x.Y)
[error]    |               Required: Test.x².Y
[error]    |
[error]    |               where:    x  is a value in class Z
[error]    |                         x² is a value in object Test
[error]    |

Is there a way to gently remind the compiler that z.x is assigned to x just one line above?

Even if (z.x == x) { val y: x.Y = z.y } does not solve the issue, even though path equivalence should be inferred from control flow.

Background: Scala 3 macro API heavily uses PDT's, and this creates huge amount of pain to deal with --- all this pain is coming from compiler's inability to infer anything, and lack of syntactic structures to explicitly control that inference.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Maxim
  • 1,209
  • 15
  • 28

1 Answers1

4

This is correct behavior for path-dependent types (by the way, in Scala 2 it's the same).

Would you be satisfied with

val y: z.x.Y = z.y // compiles

?

z.x equals x but this doesn't mean that the type z.x.Y is x.Y.

Similarly,

class A {
  type T
}
val a = new A
val a1 = a
//implicitly[a.T =:= a1.T] // doesn't compile

a equals to a1 but this doesn't mean that the type a.T is a1.T.

This is Scala 2 spec for equivalence of path-dependent types: https://scala-lang.org/files/archive/spec/2.13/03-types.html#equivalence. In our case the prefixes have different singleton types:

implicitly[z.x.type =:= x.type] // doesn't compile
  • If a path p has a singleton type q.type, then p.type ≡ q.type.
  • If O is defined by an object definition, and p is a path consisting only of package or object selectors and ending in O, then O.this.type ≡ p.type.

You can fix the compilation:

class A {
  type T
}
val a = new A
val a1: a.type = a
implicitly[a.T =:= a1.T] // compiles

and

class X {
  class Y
}

class Z(val x: X) {
  type V = x.Y // added
  val y: V = new x.Y
}

val x: X = new X
val z: Z = new Z(x)
val y: z.V = z.y // compiles

See also:

In the latest release of scala (2.12.x), is the implementation of path-dependent type incomplete?

Cannot prove equivalence with a path dependent type

Force dependent types resolution for implicit calls

How to help the Scala 3 compiler infer a path-dependent-type?

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

If you use REPL you'll see the types inferred by the compiler (removing explicit type annotations as @LuisMiguelMejíaSuárez advised in comments):

scala> class X {
     |   class Y
     | }
class X

scala> class Z(val x: X) {
     |   val y: x.Y = new x.Y
     | }
class Z

scala> val x = new X
val x: X = X@6f76c2cc

scala> val z = new Z(x)
val z: Z = Z@7e62cfa3

scala> val y = z.y
val y: z.x.Y = X$Y@52bd9a27  // notice z.x, not x
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Well, so there is no way to make this work. I cannot stick to same prefix everywhere. This simplified snippet represents the situation when I _have_ to pass the PDT to different class, and have it interoperable with outside code. It's sad that Scala has so incomplete implementation of the feature, and it's a pity that such incomplete feature is used in macros, making large macros rather difficult to write without tons of casts. – Maxim Sep 24 '22 at 10:57
  • @Maxim This is not incomplete implementation, this is intended behavior. I can't see how this can be implemented differently. Equality `a == a1` is checked at runtime (you can override `equals`). Even referential equality `a eq a1` is checked at runtime (you need JVM for that, not only Scala compiler). Types are inferred at compile time, so `a.T =:= a1.T` is checked at compile time when we can't know yet whether `a` and `a1` are the same. Maybe you should open a new question with your actual use case (macros) and we'll see if we could handle your issues more optimally. – Dmytro Mitin Sep 24 '22 at 11:06
  • It is incomplete not w.r.t. specification, but w.r.t. use cases. Compiler _could_ prove statically that two paths refers to same instance. Ok, not `==`, and not even `eq`. `val x` in `Z` is a stable identifier, it's initialized with `x`, and to best of my knowledge this should imply that `z.x` and `x` are _statically_ equivalent. – Maxim Sep 24 '22 at 11:12
  • @Maxim Thanks for clarification of your point. Then I guess you'd like singleton types `z.x.type`, `x.type` to be the same too. I still think it's better to discuss this on your actual use case. – Dmytro Mitin Sep 24 '22 at 11:27
  • 1
    @Maxim Scala 3 macros are already easier in this sense than Scala 2 macros. Compare signatures `inline def macroImpl[T](x: Expr[T])(using Quotes, Type[T]): Expr[T]` vs. `def macroImpl[T: c.WeakTypeTag](c: blackbox.Context)(x: c.Expr[T]): c.Expr[T]`. `Expr`, `Type` do not depend on context. – Dmytro Mitin Sep 24 '22 at 11:27
  • @Maxim It's possible that you can avoid casting if you introduce a generic `def foo[Q <: Quotes]` and use it like `foo[q.type]`. – Dmytro Mitin Sep 24 '22 at 11:29
  • @Maxim See how path-dependent types were handled in Scala 2 macros https://docs.scala-lang.org/overviews/macros/overview.html#writing-bigger-macros (even before macro bundles). – Dmytro Mitin Sep 24 '22 at 11:34
  • @Maxim Also https://docs.scala-lang.org/overviews/quasiquotes/lifting.html#reusing-liftable-implementation-between-universes – Dmytro Mitin Sep 24 '22 at 11:40