Adding to @DmytroMitin excellent answer, here is a step by step analysis for those with slow minds like mine :)
The explanation is based on this example code:
class Outer {
class Inner
def doSomething(a: Inner): Unit = ()
}
val outerRef = new Outer
The first thing we have to note is that the type of the reference outerRef
is not Outer
but outerRef.type
, which is assignable to Outer
but not the opposite. That happens even if the type of outerRef
were specified explicitly to be Outer
(val outerRef: Outer = new Outer
).
summon[outerRef.type <:< Outer]
The second thing we have to note is that a copy of a reference does not have the same type than the original reference.
val copyOfOuterRef = outerRef
Here the type of the reference copyOfOuterRef
is not outerRef.type
but copyOfOuterRef.type
, which is assignable to Outer
but not to outerRef.type
.
summon[copyOfOuterRef.type <:< Outer] // compiles
summon[copyOfOuterRef.type =:= outerRef.type] // does not compile
summon[copyOfOuterRef.type <:< outerRef.type] // does not compile
summon[outerRef.type <:< copyOfOuterRef.type] // does not compile
What does all this have to do with path-dependent-types?
The path-dependent-type assignability rules is based on the singleton type of the references involved in the path.
The following line defines a reference whose type is assignable to the path-dependent-type outerRef.Inner
.
val innerRef = new outerRef.Inner
Therefore, it is suitable to be the argument of the outerRef.doSomething
method.
outerRef.doSomething(innerRef) // compiles
summon[innerRef.type <:< outerRef.Inner] // compiles
But not suitable to be the argument of the copyOfOuterRef.doSomething
method.
copyOfOuterRef.doSomething(innerRef) // does not compile
summon[innerRef.type <:< copyOfOuterRef.Inner] // does not compile
because the singleton type of the references involved in the paths is not the same.
summon[copyOfOuterRef.type =:= outerRef.type] // does not compile
To solve that, we have to make the copy of the reference have the same singleton-type than the original reference.
val copyOfOuterRefWithSameType: outerRef.type = outerRef
Now the singleton type of the references that conform both paths, outerRef.X
and copyOfOuterRefWithSameType.X
, are the same. Therefore
copyOfOuterRefWithSameType.doSomething(innerRef) // compiles
summon[outerRef.type =:= copyOfOuterRefWithSameType.type] // compiles
summon[outerRef.Inner =:= copyOfOuterRefWithSameType.Inner] // compiles
A more realistic case
Usually, we can't change the type of the second reference (the copy of the first) because the first reference is out of the scope. For example, when the second reference (the copy) is a member a previously defined class.
class User(val outer: Outer) // defined in some place where the `outerRef` reference is not accesible.
val outerRef = new Outer
val user = new User(outerRef)
Here both user.outer
and outerRef
reference the same instance of Outer
but because the path-dependent-type is based on the singleton type of the references involved, the assignability fails.
val outerRef = new Outer
val innerRef = new outerRef.Inner
user.outer.doSomething(innerRef) // does not compile
summon[innerRef.type <:< user.outer.Inner] // does not compile
because
summon[user.outer.Inner =:= outerRef.Inner] // does not compile
To solve that we have to make the singleton type of the outer
member be equivalent to the type of outerRef
, which is the singleton type outerRef.type
. We can achieve that parameterizing the member type.
class UserBis[O <: Outer](val outer: O)
val outerRef = new Outer
val innerRef = new outerRef.Inner
val userBis = new UserBis[outerRef.type](outerRef)
userBis.outer.doSomething(innerRef) // compiles
summon[userBis.outer.type =:= outerRef.type] // compiles
summon[userBis.outer.Inner =:= outerRef.Inner] // compiles