-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Its is scary to use <Iterable>.firstWhere(orElse: ...)
#33841
Comments
<Iterable>.firstWhere(orElse: ...)
<Iterable>.firstWhere(orElse: ...)
Why does this happen? If you copy the implementation of E firstWhere1<E>(List<E> elements, bool test(E element), {E orElse()}) {
for (E element in elements) {
if (test(element)) return element;
}
if (orElse != null) return orElse();
throw 'no element';
}
SoundAsset firstWhere2(List<SoundAsset> elements, bool test(SoundAsset element), {SoundAsset orElse()}) {
for (SoundAsset element in elements) {
if (test(element)) return element;
}
if (orElse != null) return orElse();
throw 'no element';
}
Asset firstWhere3(List<Asset> elements, bool test(Asset element), {Asset orElse()}) {
for (Asset element in elements) {
if (test(element)) return element;
}
if (orElse != null) return orElse();
throw 'no element';
} (I was wondering if |
tl;dr: It happens because of Dart's unsafe covariant class generics combined with static inference. The signature of Now, if you have a assets.firstWhere((a) => a.name == 'A', orElse: () => assets.first) where We are allowed to call If the type system was smarter (probably way smarter than we can ever expect), it could recognize that the type of That's what happens when you add the extra type parameter: void printBestAsset<A extends Asset>(List<A> assets) {
assets
.firstWhere((a) => a.name == 'A', orElse: () => assets.first)
.printMe();
} Here you say that the list is a What would actually solve the problem would be to declare T firstWhere<T super E>(bool Function(E) test, {T Function() orElse}); Then you would be able to say which asset type you want, as long as it includes both the elements of the list and the value returned by |
Thanks for the detailed explanation! :-) I think I'd overlooked this:
I actually ran this code this in C# before commenting and didn't get any errors - but I just tested again and realise that it was because I'd used an
I'm not sure I'd ever come across this before and always assumed it would've failed to compile (as it does with a Edit: Heh, apparently it's Java's fault C# has it, and it seems to be regretted =)
|
Is there a particular reason we can't change, say, T firstWhere<T extends E>(bool Function(E) test, {T Function() orElse}); |
I was wondering that; but wasn't sure if |
@lrhn Why don't we have super-bounded types? |
An existential open would help: void printBestAsset(final List<Asset> assets) {
assets as List<?T0>;
// Declares `T0` as a fresh type variable and binds it to the
// actual type argument of `assets`, such that
// `assets is List<T0>` is true and no subtype `T` of `T0`
// will make `assets is List<T>` true. We know `Asset <: T0`.
assets
.firstWhere((a) => a.name == 'A', orElse: () => assets.first)
.printMe();
// Given that `orElse` has type `T0 Function()` and `assets`
// has type `List<exactly T0>` (even if we haven't introduced
// syntax for that, the type checker can still know that this type is
// invariant), there will be no downcast when the function literal
// is passed to `firstWhere`, and no downcast in the function
// literal when it returns `assets.first`.
} This allows us to call Compared to Java wildcards, this is just as flexible, but more powerful, because we can abstract away from knowledge which has more structure and later regain it (e.g., if we have a |
@matanlurey T firstWhere<T extends E>(bool Function(E) test, {T Function() orElse}); because it's not type-safe. If we pick a The constraint T firstWhere<T super E>(bool Function(E) test, {T Function() orElse}); we know that It is also a problem that With suitable language extensions, maybe we could type it as: <T|E> firstWhere<T>(bool Function(E) test, {T Funcition() orElse = const (_) => null});
// pr
<T|E> firstWhere<T = void>(bool Function(E) test, {T Funcition() orElse}); That would allow us to specify the exact type of what we get back, independently of As for why we don't have super-bounds, I'll leave that to the type-system experts, but I have heard rumors about potential undecidability. Maybe it's not a problem (Java survived it), but maybe it is when you have first class generic function types. I don't think there is anything we can reasonably do to improve the typing of the method with the current type system. If/when we get non-nullable types, we might need to reevaluate it, because it would be nice to be able to write |
Filing this under language. Is there a tag for covariance issues? One simple possibility here is to provide an analyzer flag (similar to no-implicit-casts) that warns on covariant up-casts. E.g., this: final assets = [SoundAsset('A'), SoundAsset('B')];
printBestAsset(assets); becomes: final assets = [SoundAsset('A'), SoundAsset('B')];
printBestAsset(assets as List<Asset>); It's a bit odd as it's an upcast and "safe" (in the sense that the cast itself cannot fail), but it does indicate to the user that something potentially unsafe (a later runtime failure due to covariance) is happening. |
Related: #54108 |
Similar issues:
Consider the following, fairly simple example of using
<Iterable>.firstWhere
:This properly prints, as expected,
A <Asset>
. Neat! Let's get polymorphic.I looked at this with 3 other Google/Dart team engineers, until @alorenzen mentioned "well, that's why Java has
? extends T
". Here is the page from "Effective Java", highlighting exactly that:... and we realized we could write this in Dart too:
... though, contrast this with Java's API of
List<? extends Asset>
, which seems more obvious.Questions:
The text was updated successfully, but these errors were encountered: