-
Notifications
You must be signed in to change notification settings - Fork 205
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
Current "covariance by default" paradigm is not respected by the compiler #2322
Comments
I think the "proper" workaround is take<num>(<double>[]); // OK, no lints You're correct that Dart gets confused when you imply you want The difference is where you correct Dart. One way to do it is to cast your list back to a I don't know what's up with the |
What @Levi-Lesches says. A The |
Thanks for the detailed explanation! I see now the problem is with inference and how to tell the compiler what we want. Thus proposed solution: take<num>(<double>[]); // OK, no lints Is very clever and works perfectly for the example I provided. This simple syntax is what I was looking for, and I forgot that you could specify the desired generic type on the function call level. However, I believe upon revising my question, I ended up simplifying my example too much. My actual complete use case was an extension function: extension ComparableIterable<T extends Comparable<T>> on Iterable<T> {
void fn() { /* .. */ }
}
void main() {
(<num>[]).fn(); // ok
(<double>[]).fn(); // not ok -- does no compile
(<double>[] as List<num>).fn(); // ok but compiler wants to "auto-fix"
} And when writing the question I at some point thought that fact was irrelevant and converted to a normal function. My problem is that I don't know how to specify the generic parameter used by the receiver of an extension function. Is there a similar workaround in this case? |
I'd like to comment a little on the reliance on dynamic type checks in relation to covariance. @luanpotter wrote:
This is still true. The declaration-site variance proposal is still active, and I hope it will be fully implemented as soon as possible. I believe it's a matter of priority rather than a matter of disagreement, but we do need to sort out some corner cases about mixed inheritance (like The language Dart is sound in the sense that it strictly enforces that the heap is well-typed. That is, for every variable Similarly, an expression with static type However, some expressions may throw an exception at run time because of a type error, even though the program does not have any compile-time errors or warnings. For example, So I think it's fair to say that Dart doesn't have a "wrong" treatment of covariance (or any other property of the type system), but it does use a typing discipline which is considerably more permissive than it otherwise could, because it performs some type checks at run time rather than compile-time. Still, I'd love to have support for the traditional and well-known approach to variance which is described in #524. I'm actually deeply surprised that there are so few reports (and so few complaints ;-) about run-time type errors caused by covariance. Anyway, the other topic is F-bounded type variables, and in particular the fact that the type inference algorithm does not handle the situation where an F-bound can be shown to be satisfied by using variance and then verifying the F-bound: class A<X extends A<X>> {}
class B extends A<B> {}
class C extends B {}
void f<X extends A<X>>(X x) {}
void main() {
f(C()); // Error: Type inference failed.
} It seems likely that it would be costly to perform a traversal of the entire superinterface graph in order to find a supertype (starting from However, Kotlin type inference does handle this task (which would presumably be very similar to the corresponding type inference task in Dart): open class A<X: A<X>>() {}
open class B() : A<B>() {}
open class C() : B() {}
fun <X: A<X>>f(x : X) {}
fun main() {
f(C()); // Accepted, that is, it infers `f<B>(C())`.
} We'd need to consider F-bounds involving more than one class, of course: class A1<X extends A1<X, Y>, Y extends A2<X, Y>> {}
class A2<X extends A1<X, Y>, Y extends A2<X, Y>> {}
class B1 extends A1<B1, B2> {}
class B2 extends A2<B1, B2> {}
class C1 extends B1 {}
class C2 extends B2 {}
void f<X extends A1<X, Y>, Y extends A2<X, Y>>(X x, Y y) {}
void main() {
f<B1, B2>(C1(), C2());
f(C1(), C2()); // Error: Type inference failed.
} @leafpetersen, do you think we could detect and "delay" constraints that are F-bounds during type inference, and find a result which is satisfying all non-F-bound constraints first, and then search for a supertype that satisfies the F-bounds, and then check whether the resulting type arguments produce a type correct expression? In the case where we have multiple type parameters that participate in an F-bound, we'd have a tuple of types |
Yes. One trick with extensions is that when it's not being recognized, you can specify it by name, as though it were a wrapper: ComparableIterable(<double>[]).fn(); Now this is similar to calling a function directly, and again we get the problem that ComparableIterable<num>(<double>[]).fn(); |
After some digging around old issues, I can see there has been some discussion in the past about providing a more robust type system to Dart by including specific covariant/contravariant type definitions as provided by other languages (java, kotlin, etc).
That being said, it is my understanding that, currently, the language design is to consider everything to be "covariant by default":
(from this great issue from 2019).
Which is not, as discussed, sound (nor safe) -- and I would love to see that addressed. But I am not sure the status of these proposals, as there are many open, old issues from the community about it. And while I would much prefer that to be the answer to this, I believe that even when considering the current rule, there is an issue with the compiler implementation (if I am not misinterpreting these statements).
Assuming the "covariant by default" rule, a
List<double>
should be aList<num>
, for all intents and purposes:However, this does not work when the generics are a bit more involved. See an example below: (dartpad):
Here, my function generics satisfy
List<num>
. But for some reason, it does not compile forList<double>
. Of coursedouble
itself does not satisfyT extends Comparable<T>
, butnum
does. I believe the current "covariance by default" rule is being violated here, as myList<double>
should be treated as aList<num>
to satisfy the type constraint.To make it even more weird, see how I can cast the
List<double>
toList<num>
and then it totally works! But then the compiler correctly points out that since dart is "covariant by default" the cast is unnecessary, and the IDE will auto-fix to remove the cast. This removal actually causes the code to break.The way I am understanding this, this is a violation of the current rule, but as pointed out in many issues, the current rule isn't good to begin with. So my question/proposal is:
T super U
syntax, or explicit covariant/contravariant modifiers like kotlin'sin
,out
,inout
?Because if not, I believe this should be treated as a bug as
take(<double>[])
should work with the current ruling, as far as I understand it. And that might be a much quicker fix than to get the actual spec sorted out. Also, having the compiler warning/auto-fix generate non-compiling code is odd.The reason I am asking this is that this not an abstract, far-off, edge case that I machinated; having a function that takes a list whose elements can be compared seems to be like a very common use case (e.g. sorting, finding min, max, etc). And not being able to pass a list of
double
orint
because of how they implementComaprable<num>
limits our design choices.If there are approved plans to solve this by the root cause though -- and make the type system more sound--, I am happy to wait for that solution instead, as that would allow for even more cool & safe code :)
The text was updated successfully, but these errors were encountered: