Let's define a trait with an abstract class
object Outer {
trait X {
type T
val empty: T
}
Now we can make an instance of it:
val x = new X {
type T = Int
val empty = 42
}
Scala now recognizes, that x.empty
is an Int
:
def xEmptyIsInt = x.empty: Int
Now, let's define an other class
case class Y(x: X) extends X {
type T = x.T
val empty = x.empty
}
and make an instance of it
val y = Y(x)
But now Scala, isn't able to infer that y.empty
is of type Int
. The following
def yEmptyIsInt = y.empty: Int
now produces an error message:
error: type mismatch;
found : y.x.T
required: Int
y.empty : Int
Why is this the case? Is this a scala bug?
We can mitigate this issue by introducing a parameters to the case class:
case class Z[U](x: X { type T = U }) extends X {
type T = U
val empty = x.empty
}
Then it works again
val z = Z(x)
def zEmptyIsInt: Int = z.empty
But we always have to mention all the types inside X at call-site. This ideally should be an implementation detail which leads to the following approach:
case class A[U <: X](z: U) extends X {
type T = z.T
val empty = z.empty
}
This also mitigates the issue
val a = A(x)
def aEmptyIsInt: Int = a.empty
}
So, to summarize, my questions are the following: Why does the simple case doesn't work? Is this a valid workaround? What other problems might come up when we follow one of the two workaround approaches? Is there a better approach?
You've re-used x
for different things, so from here on I'll call the object instantiated by val x
"instance x" and the x: X
used in class Y "parameter x".
"Instance x" is an anonymous subclass of trait X
with concrete members overriding trait X
's abstract members. As a subclass of X
you can pass it to the constructor for case class Y
and it will be accepted happily, since as a subclass it is an X
.
It seems to me you expect that case class Y
will then check at runtime to see if the instance of X
it is passed has overridden X
's members, and generate an instance of Y
whose members have different types depending on what was passed in.
This is emphatically not how Scala works, and would pretty much defeat the purpose of its (static) type system. For example, you wouldn't be able to do anything useful with Y.empty without runtime reflection since it could have any type at all, and at that point you're better off just using a dynamic type system. If you want the benefits of parametricity, free theorems, not needing reflection, etc. then you have to stick to statically determined types (with a small exception for pattern matching). And that's what Scala does here.
What actually happens is that you've told Y
's constructor that parameter x
is an X
, and so the compiler statically determines that x.empty
, has the type of X.empty
, which is abstract type T
. Scala's inference isn't failing; your types are actually mismatched.
Separately, this doesn't really have anything to do with path-dependent types. Here is a good walkthrough, but in short, path-dependent types are bound to their parent's instance, not determined dynamically at runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With