-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Allow variadic control of generics (i.e. List) #33812
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
This is working-as-intended. There are smarter folks to give the gritty details, but basically: var a = ['string']; ... is the same as ... List<String> a = <String>['string']; While: List<dynamic> a = ['string']; ... is the same as ... List<dynamic> a = <dynamic>['string']; This might not make sense in this particular example, but imagine the following: List<num> evilNumbers = [1, 7, 13];
evilNumbers.add(99.9); With your suggestion, this would be invalid code, because we'd create a T fetch<T>() => /* Implementation Detail */
void main() {
String name = fetch(); // == fetch<String>();
example(fetch()); // == example(fetch<List<int>>());
}
void example(List<int> numbers) {} I think the frustration you're feeling is due to the use of I think your particular question is why extracting an argument changes the behavior of a program, and that is (un)fortunately because most of the time (I'd say, 95 to 99%, or more), you want the collection or generic inferred/reified using the most data available. In the case of final b = ['string'];
// Would be a runtime error, List<dynamic> is not a List<String>.
List<String> c = b; Hope that helps! |
Thanks for writeup Matan. I think we're talking about something similar, but not quite (also, don't worry, not frustrated - just raising a flag). To clarify, I agree that the type inference behavior makes sense. I am claiming that the type system is incorrect here, and I don't have a suggestion on its resolution. I'll use another example, without dynamic. Let's say I have a function that takes a void f(num n) {
// I've been guaranteed n is an instance of num (or null)
} However, if the argument is // This will fail if arg is List<int>
void f(List<num> list) {
list.add(0.0);
} Contrast this with down-casting a collection. I cannot pass a |
I'm not a language expert and definitely feel free to wait for @leafpetersen (we should try and help capture some of this conclusion on the language site, if possible), but what you are talking about is covariance and contra-variance. In Dart1, For generic types specifically, the types unfortunately are only very useful (statically) for reads. Contrast this with Dart1, where the types were almost always safe for writes, but you could not guarantee something annotated with // Legal, and common, Dart 1.
void main() {
var x = [];
x.add('I am not a number silly');
example(numbers);
}
void example(List<int> numbers) {
for (var x in numbers) {
print(x is num); // Might be False (in this case it is!)
}
} ... produces no static or runtime errors, until Dart2 guarantees any annotated type, including generics, when read, is that type (or "greater"): void main() {
var x = [0, 0.5, 1]; // Implicit List<num>
}
void example(List<int> numbers) {
for (var x in numbers) {
print(x is num); // Will always be True
}
} A side-effect of this choice is that writes have to do a little extra work. In your case: void f(List<num> list) {
if (list is List<int>) {
return; // Or throw, expected List<num> or List<double> etc.
}
list.add(0.0);
} If Dart ever gets ways to control variance ( 🙏 🙏 🙏 ), you could write something like: void f(List<in num> list) {
// Guaranteed that 'list' is EXACTLY a List<num>, not a List<int> or List<double>.
} FWIW, there is a fairly famous S/O question on almost this: |
I think we are saying the same thing. If I take the example from the accepted S/O answer and convert it to Dart, I get a runtime exception and the analyzer can't tell me about it beforehand. void main() {
List<int> arr = [1, 2, 3];
List<num> arr2 = arr;
arr2.add(2.54); // throws at runtime
} If the solution is that the language needs variance specifiers, and that collection types have to use these specifiers, awesome. But as it stands now, I can write code that is always invalid at runtime and I don't get a warning. |
@joeconwaystk:
For sure! Just trying to help this issue turn into something actionable :)
FWIW, in Java using
That's the case across the spectrum of languages similar to Dart too, including Java. The goal of Dart2, unfortuantely, is not lack of errors, but rather, heap soudnness - so that optimizing compilers, if presented with a There are many other places in Dart2 runtime errors are possible: void main() {
List<String> x = [];
List<Object> y = x;
List<int> z = y; // Error: List<String> is not a List<int>
} void main() {
String x;
// NPE
print(x.substring(0, 1));
} We are exploring how to tighten type system and static checks without waiting for a "Dart 3.0" (#33749), it would definitely be useful to add your experiences and requirements. Some other popular threads include: |
@matanlurey - the S/O article Why are Arrays invariant, but Lists covariant? is for Scala not Java. As you know Scala has a "strong static type system" and "support for functional programming". As the answer points out, the Scala Your example above converted to Scala (with explicit typing) does not compile (which is good):
As a developer, I appreciate the compiler catching the type mismatch but also letting me explicitly override if I need to. The dartz contributed package https://github.com/spebbe/dartz provides immutable collections (IVector, IList, IMap, ISet) and containers (Option, Either, Tuple). We are using this package in a project that we're converting from Typescript. Our backend is written in Scala so the hybrid object/functional style is familiar to us. All of our data structures are immutable and strongly typed. The lack of support for covariance and contravariance is one of our biggest ongoing headaches. To add to the headache, the variance issues show up as a runtime casting error that has a useless stack trace. Better support for |
The issue you are hitting here is that Dart generics are covariant and not safe. You are saying that you don't think the advantages outweigh the disadvantages. You are not alone, other people want the type system to be completely safe too. There are also people who do use the unsafe-but-convenient features and don't want that use-case to get more complicated. Immutable collections are actually a case where invariance works because you never write to them. I don't think we have any current plans of changing the default, but at some point we might want to investigate whether we can allow other variants as opt-in. |
@lrhn I didn't say anything about relative advantages and disadvantages. I did point out that I appreciate the (Scala) compiler finding unsafe type usage but also allowing me to override when (I think) I know better. I continue rambling about immutable data structures and the challenges we've encountered due to incomplete variance support in Dart. I was trying to address @matanlurey previous comment to "add your experiences and requirements". Your comment that "Immutable collections are actually a case where invariance works" is a bit misleading. Invariance (i.e. not covariant and not contravariant) always works for both mutable and immutable collections. As you point out, mutable collections can be used with different variances as long as developers don't break things (e.g. use invariantly or immutably). Immutable collections are always safe to use covariantly (okay one link to a variance description). So my rambling is really just another request for Dart to support variance specifiers. I'm by no means a language expert but I have appreciated languages (e.g. Scala) that just seem to do the right thing and keep me from being stupid. This can also be said for immutable data structures. |
@rich-j wrote:
OK, I'll comment a bit on that. ;-) During the work on designing wildcards and writing Adding Wildcards to the Java Programming Language, we were certainly aware of the history, including unsafe variance and safe covariance (the latter was published in 2006, but it is based on a language, gbeta, which had been using that approach for about 10 years). Later, when Dart was designed, all classes were made covariant in all type arguments, and "covariant parameters" are hence subject to dynamic checks—not by accident, but in order to strike a different balance between the complexity of type annotations in source code and the amount of dynamic checking. I've been pushing for support for use-site invariance declarations (let's just assume that we would use a modifier spelled 'exactly'): List<exactly num> xs = ...;
xs..add(42).add(4.2); // Safe so even though this issue was closed with 'working as intended' (which is true for Dart of today), we might be able to express at least invariance at some point in the future. Aside: I don't think contravariance is equally important: If some entity should work as a sink, use a function! (The parameter types of a function type are contravariant). Nevertheless, I actually think that it makes sense for Dart to use the covariant types by default, even though it introduces some potential type errors at run time. The point is that this works quite nicely in situations where an instance, say, a Later in the life of said object it may be accessed via covariant supertype (say, as an In short, a style where objects subject to covariance are handled in a near-functional manner (create and populate it in a narrow "owner" context, then use it in "client land" as a read-only entity), those dynamic errors will be rare in practice. You could also say that there's a very subtle nudging effect in the direction of using that style. ;-) Still, if we have an |
The following code shows two seemingly identical-in-behavior code blocks, but one is a type error and the other is not. If I am misunderstanding something, please feel free to close with minimal comment.
The method
f
takes aList<dynamic>
and adds aint
to it. Block 1 succeeds, and block 2 fails with a type error. Both blocks pass the same list literal argument. The difference is that the failing block first assigns the list literal to a local, inferred variable before passing it to the function.In block 1, the argument's type is inferred to be
List<dynamic>
because of the location of its instantiation. In block 2, the argument's type is inferred asList<String>
first, before being successfully upcast toList<dynamic>
. But within block 2, this makes adding anint
illegal.I think this is surprising: extract an argument into a local variable and the behavior of the program would change. I also think it invalidates some guarantees about the type of a variable in a given lexical scope.
The text was updated successfully, but these errors were encountered: