Skip to content

Buggy or undocumented behavior concerning subtyping relationships with unions, intersections and variance #5980

Closed
@anatoliykmetyuk

Description

@anatoliykmetyuk

Not sure if it is a bug or an unintuitive/undocumented behavior.

trait A
trait B

trait Covariant[F[+_]] {
  trait G[+X]

  def fx: F[A  &   B] = ???  // fy: Type Mismatch Error
  def fy: F[A] & F[B] = fx

  def gx: G[A  &   B] = gy
  def gy: G[A] & G[B] = gx
}

Above, fx = fy gives a type mismatch error. However, gx = gy does not. My guess is, in case of fx = fy, the only fact the compiler knows about F is that it is covariant. According to the covariance laws, fx = fy is illegal. However, in case of gx = gy, the compiler knows the actual type G. It hence can compute what exactly members should G[A & B] and G[A] & G[B] have, and see that these members are identical, and hence allow for gx = gy.

However, this logic does not stand the contravariance test:

trait Contravariant[F[-_]] {
  trait G[-X]

  def fx: F[A  &   B] = fy  
  def fy: F[A] | F[B] = ??? // fx: Type Mismatch Error

  def gx: G[A  &   B] = gy
  def gy: G[A] | G[B] = ??? // gx: Type Mismatch Error
}

Here, fy = fx is illegal according to contravariance laws, and the fact that F is contravariant is the only thing the compiler knows about F. However, if the Covariant example reasoning above is correct, then the compiler will take into account the information available about the concrete G trait. Following the covariant example logic, in case of gy = gx, the compiler should be able to compute that G[A] | G[B] type has the same members as G[A & B]. And, if gx = gy is allowed for the covariant case, following the same logic, gy = gx should be allowed for the contravariant case. However, this does not happen, and the compiler appears to be guided only by the contravariance laws, ignoring this time the distinction between the type parameters and traits.

A similar case makes its appearance in the documentation on intersection types, precisely, the snippet of:

val x: A & B = new C
val ys: List[A & B] = x.children

There, the documentation says:

The type of children in A & B is the intersection of children's type in A and its type in B, which is List[A] & List[B]. This can be further simplified to List[A & B] because List is covariant.

The simplification can indeed be done when defining C, so that the children method returns List[A & B], because List[A & B] <: List[A] & List[B]. However, what exactly happens in case of val ys: List[A & B] = x.children?

val x: A & B = new C
def f(x: C) = ???
f(x)

The above fails with

[error] 14 |  f(x)
[error]    |    ^
[error]    |    Found:    (A & B)(Official.x)
[error]    |    Required: C

So, there is no chance that when doing val ys: List[A & B] = x.children, the compiler knows that x is of type C. The compiler thinks that x is of type A & B. And what should the type of children of A & B be? According to the doc on the intersection types:

If a member appears in both A and B, its type in A & B is the intersection of its type in A and its type in B. For instance, assume the definitions:

Hence, the type of children in A & B must be List[A] & List[B], and List[A] & List[B] >: List[A & B]. I can't see any reason for the compiler to make the type in question a more special List[A & B].

Hence, the assignment val ys: List[A & B] = x.children should not be possible, because x is of type A & B and x.children is inferred to List[A] & List[B], and List[A & B] <: List[A] & List[B].

One way it can be possible is if the compiler utilizes the information it has about the List type and computes that List[A] & List[B] =:= List[A & B]. However, this does not appear to be true:

[error] 15 |  implicitly[List[A & B] =:= List[A] & List[B]]
[error]    |                                               ^
[error]    |no implicit argument of type List[A & B] =:= List[A] & List[B] was found for parameter ev of method implicitly in object DottyPredef

And, if this was the case, then why does it not work with the union type as described above?

So, is it a bug, or is there some logic regarding subtyping rules with covariance/contravariance and unions/intersections that the documentation does not mention?

I tested the above code on dotty 0.13.0-RC1 and 0.14.0-bin-20190222-789269d-NIGHTLY.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions