Unification across intersection of conflicting base types is asymmetrical
See original GitHub issueI noticed this in real code in Scala 2.x, and it seems to exist in Dotty (0.7.0-RC1), too, though I haven’t explored whether it can lead to a genuine unsoundness issue… but it at least breaks an invariant I wouldn’t expect:
trait F[T]
def foo[T](f: F[T])(g: T) = g
We can then successfully call,
foo(null.asInstanceOf[F[String] & F[Int]])("")
but not,
foo(null.asInstanceOf[F[String] & F[Int]])(42)
It appears that when trying to instantiate the type parameter T
from the intersection type, the unification algorithm simply takes the first match it finds without checking for subsequent matches in the intersection, rather than inferring the union type, String | Int
, which I think would be the only consistent answer. The LUB of String
and Int
might be better, but it’s still wrong if F
is invariant in T
.
Note that it is impossible to construct an instance of F[String] & F[Int]
, but I don’t think that’s sufficient protection…
Issue Analytics
- State:
- Created 5 years ago
- Comments:15 (15 by maintainers)
Top GitHub Comments
No, it doesn’t. We approximate unions because they’re generally too precise and not what you want to work with (just like we widen singleton types), no such problem arise with intersections.
That was my intuition speaking overconfidently… but it might be worth me going over my “logic” step-by-step, so I can get a better understanding. Everything I know about types comes from experimentation, so I appreciate any feedback!
F[T]
represents all values which satisfy a set of properties which are written in terms ofT
, for anyT
.F[A] & F[B]
represents all values with the same set of properties, each of which should be satisfied both whenT = A
or whenT = B
.T = A | B
”, however in my logical leap of assumption I have ignored that each type is templated in terms of a single type,T
, and functional dependencies may exist between properties of that same type (in both covariant and contravariant positions), where substitution ofA
everywhere orB
everywhere will work, butA | B
will, in general, not.That was a useful exercise - thanks for instigating it!
The asymmetry in LUBs/GLBs in inference is particularly interesting to me, as I’m often making use of LUBs and GLBs to construct new types, and I rely heavily on them working differently in Scala… and having union types (“the union of the set of all the types”) as well as supertype LUBs (“the intersection of the set of all the supertypes”) confuses me (and presents the possibility of a number of in-between types formed from some hybrid of the two), while in the dual world, we only use intersection types for the GLB (i.e. “the intersection of the set of all the types”) rather than subtype GLBs (“the intersection of the set of all the subtypes”). But we construct types from the top-down, with the interesting types towards the bottom, so we don’t usually get to work with many types which have useful common subtypes. Anyway… I’ll carry on thinking about this to myself. 😃