-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Make all match types invariant #6047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I think the right approach is to make occurrences of type variables in scrutinee and patterns non-variant and to leave occurrences in the right-hand sides co-variant. |
I'm not sure I'm following. The issue I'm raising is about the match type itself being covariant. Currently, by writing a Having variance in the type variable of scrutinee and patterns should be fine: that simply denotes a larger set of value. The algorithm implemented in #5996 accounts for those. |
The way variance checkin works is that you traverse the type and characterize positions as non-variant, covariant or invariant. A type parameter declared co-variant may only occur in covariant positions, and so on. Here's an example:
That one is OK. So it depends where a type variable occurs. Unfortunately, even this refined criterion breaks Tuple.scala in 5 locations. E.g.
Both cases would be rejected, but they are not real errors, since the types are covariant. |
Let's analyze why
So that means, if X <: X1 <: Tuple, then Concat[X, Y] <: Concat[X1, Y] and it's safe to take But it's very subtle. We need both static reducibility (i.e. the bound of a type guarantees that the match always succeeds) and non-overlapping patterns (guaranteeing that the same case is chosen for |
What about this proof that Unit & (Int *: Unit) <: (Int *: Unit)
=> (by covariance of Concat on its first type parameter)
Concat[Unit & (Int *: Unit), Unit] <: Concat[(Int *: Unit), Unit]
=> (by match type reduction)
Unit <: (Int *: Unit) |
Touche. I did not think about empty types. Yes, they ruin the argument. But then how do we formulate a useful version of |
What's the use case for variance here? The invariant |
When reducing a covariant match type Wouldn't it be cleaner if match types used exact type patterns? As in, This would allow tuples to be covariant, because when matching |
I think that would be problematic as soon as we try to type values with match types. For example, it would be nice to be able to type check the following: def foo[X](x: X)
: X match {
case Int => 1
case _ => 2
}
= x match {
case _: Int => 1
case _ => 2
} |
@OlivierBlanvillain I don't see how that would work anyway. EDIT: I guess you meant something like this, which I wrote using a def foo(x: Any)
: x.type match {
case _ <: Int => 1
case _ => 2
}
= x match {
case _: Int => 1
case _ => 2
} |
To start, here are some cases from the tuples1.scala test that fail to compile: def concat[X <: Tuple, Y <: Tuple](x: X, y: Y): Tuple.Concat[X, Y] = x ++ y
def head[X <: NonEmptyTuple](x: X): Tuple.Head[X] = x.head I believe most people would expect these cases to work. What one has to do is the following: def concat(x: Tuple, y: Tuple): Tuple.Concat[x.type, y.type] = x ++ y
def head(x: NonEmptyTuple): Tuple.Head[x.type] = x.head That works to some degree, but lacks a generalization capability. I.e. all the types we compute are forever tied to concrete singleton types. I don't think this is good enough. |
Another approach that avoids the problem of empty types might be to add the rule:
With that rule we might take scrutinee occurrences of type variables to be covariant, provided patterns are non-overlapping. That would let us leave Tuple.scala in the form it is. There's still a tricky question to answer. Say you have a match alias F[X] = X match {
case Int => String
case String => Int
} Then F[Int] = String, but F[Any] does not reduce. However, Maybe we could equate a match type
semantically with the union of all reduced types of |
Interestingly, the union of all reduced types of SS match {...} where SS is a subtype of S is the dual of parallel reduction. Parallel reduction means we can fire more than one rule and intersect the results. The new rule would mean we take a non-deterministic choice of all rules that might match. I am seeing more and more similarities with our understanding of GADTs. Here's an attempt at new match type reduction rules that bridge the gap: A match type
reduces to
where
This aligns with our approach to GADTs and gives a justification for our handling of empty types. Indeed, if /cc @AleksanderBG |
My intuition is that it's better not to have sequential reduction on the type level, so the "union of all cases that could possibly match" approach is attractive. True, at runtime match reduction is sequential. But that's relevant for the type system only insofar that the type we compute from a match expression or match type must be a supertype of the type we reduce to at runtime. And the union approach delivers oin that. |
To me this looks like type inference and inlining working poorly together. In the following, dotty will happily infer both type ++[x <: Tuple, y <: Tuple] <: Tuple =
x match {
case x *: xs => x *: ++[xs, y]
case Unit => y
}
def ++[X <: Tuple, Y <: Tuple](x: X, y: Y): ++[X, Y] =
(x match {
case x *: xs => x *: ++(xs, y)
case () => ()
}).asInstanceOf[++[X, Y]]
def concat1[X <: Tuple, Y <: Tuple](x: X, y: Y): ++[X, Y] = ++(x, y)
def concat2[X <: Tuple, Y <: Tuple](x: X, y: Y): ++[x.type, y.type] = ++(x, y)
def concat3(x: Tuple, y: Tuple): ++[x.type, y.type] = ++(x, y) My gut feeling is that at this point all the focus should be on correlating match types with expressions. Anything related HKT, variance, inlining, kind polymorphism is just less important than being able to use match types in programs without casts... Which is why I'm suggesting Plan A: make all match types invariant. |
I think the fact that current tuples are "stuck to singleton types" under nonvariance has to with the fact that their implementation uses
Agreed on that! Plus: keep tuples working.
I am not sure that follows 😉 |
Good news: Tuples could be adapted to live in a non-variant world. #6050 implements variance checking rules for match types and adapts Tuple.scala to conform to them. I believe we should still investigate the match-reduction with union-types idea. |
Covariant match types should be removed as they can lead to contractions. For example:
Int & String <: String
Id[Int & String]
reduces toInt
,Id[String]
reduces toString
Id
we can deduce thatInt <: String
, which is wrong.The text was updated successfully, but these errors were encountered: