-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Runtime Error on Inferred Type For Closure In List with orElse
#60009
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'd probably do: That doesn't change whether inference has a problem. The receiver for E firstWhere(bool Function(E element), {E Function()? orElse}) With Try changing the argument to |
Summary: Type inference fails for lists of closures used with |
I'm a bit confused with your response, but I think my original post was confusingly framed and may have misled you. So to clarify: The following scenario throws a runtime error if the list ( return firstWhere((vFunc) => vFunc.call(input) != null, orElse: () => (_) => null).call(input); The following is the rewrite I used to avoid the bug and improve code readability (as you pointed out, the original code is confusing and needlessly calls the function twice). This rewrite does not throw any exceptions and behaves as I expect: return map((vFunc) => vFunc.call(input)).firsWhere((error) => error != null, orElse: (_) => null); In the first scenario where This error isn't blocking us in production as I just moved forward with the rewrite, but I still wonder if I misunderstood something in the original code or if there is in fact a bug/limitation in the type inference. Your suggested resolution ( |
This is one of several issues where a run-time type check fails because of dynamically checked covariance. Here is a version of the original example which has been simplified to do fewer things (it just does the thing that fails now), and output some extra information: void main() {
final badAppleValidator =
(input) => input == 'apple' ? 'Apple is a banned word' : null;
final validatorsWithInferredType = [badAppleValidator];
print(validatorsWithInferredType.validate('moose'));
}
extension RunValidations<T> on List<String? Function(String?)> {
String? validate(String input) {
this.checkCovariance();
return firstWhere(
(vFunc) => vFunc.call(input) != null,
orElse: () => (String? _) => (null as String?),
).call(input);
}
}
extension<X> on List<X> {
bool get isCovariant {
List<X> helper = this.take(0).toList();
try {
helper.addAll(<X>[]);
} on TypeError catch (_) {
return true;
}
return false;
}
void checkCovariance() {
if (isCovariant) {
print('Covariance detected: $runtimeType <: List<$X>');
}
}
} This program runs and produces the following output:
The important point is that the list As @lrhn mentioned, the context type for the argument to I added those types to the function literals to make it explicit, but you can check that this is indeed the result of the type inference by capturing this function literal and printing its The problem is that it is unsafe to pass an actual argument to As mentioned, the run-time type of However, the return type of the actual argument is (The typing gets a little bit convoluted here, but we can use a substitutability argument to justify that You can create a safer variant of extension<E> on List<E> {
E safeFirstWhere(bool test(E element), {E orElse()?}) {
for (E element in this) {
if (test(element)) return element;
}
if (orElse != null) return orElse();
throw "Some suitable exception";
}
}
void main() {
List<num> xs = <int>[1, 3];
print(xs.safeFirstWhere((i) => i % 2 == 0, orElse: () => 2.5)); // OK.
print(xs.firstWhere((i) => i % 2 == 0, orElse: () => 2.5)); // Throws.
} The point is that The fact that extension methods are working with compile-time types is a weakness in many cases, but when it comes to Another way to avoid run-time type failures caused by dynamically checked covariance is to avoid the covariance in the first place. This topic is addressed by proposals like dart-lang/language#524 and dart-lang/language#229. With those approaches you're given support for a discipline where the covariant typing simply can't arise. For example (using dart-lang/language#229), if a list is declared to have type |
Thanks for the extremely detailed response! It was helpful though I'll fully admit the complexity here has lost me. I guess from my perspective as a user who doesn't understand the type system, I expect type errors to always be compile time errors and not runtime errors (assuming I'm not using any escape hatches like |
Thanks for the kind words!
Right, constructs using However, the underlying static typing isn't designed in the same manner in all languages, and there will always be some phenomena that are deemed so unhealthy that a program has to stop (and, for instance, throw an exception), and the type system could have caught it at compile time. For instance, some languages dealing with very limited resources can make memory management a compile-time decision. Almost all other languages will have 'Out of memory' as a possible run-time failure. Similarly, C# and Java can have dynamically checked covariance errors (ArrayStoreException and ArrayTypeMismatchException), and lots of languages can have null related errors, because they do not make nullability a part of the type system. The unusual choice when it comes to Dart is that every type parameter is dynamically checked covariant. This implies that mutating operations will be subject to run-time type checks when the actual type argument (say, of a
Hmmm, dynamically checked covariance was a design decision which has turned out to work well in the vast, vast majority of cases (even though I'd very much like to be able to avoid it, per dart-lang/language#524 and dart-lang/language#229). But it's not because it is difficult to make every type variable invariant (as in the traditional approach, which is fully type checked at compile time), it was an actual design decision. The point is that invariance is really inconvenient in an OO language in many cases, and fully explicit variance control (e.g., wildcards in Java) gets complex very quickly. So I'd join you in wishing that we can turn these run-time type errors into compile-time type errors (in particular, with something like those 524 and 229 proposals). But I wouldn't expect all Dart code to turn into statically checked variance only. It's simply not that much of a problem. PS: You'll have absolutely no problems finding some math guys who think that's a scandal, and we just don't know better. ;-) |
Using
orElse
with a list of closures with inferred type produces a runtime type error:TypeError: Instance of '() => (String?) => Null': type '() => (String?) => Null' is not a subtype of type '(() => (dynamic) => String?)?'
Note that my construction of this bug is pretty awkward, the correct solution in my code was to refactor the validate method to be more readable and not to hit the bug, but I figured I'd file this issue anyway as this feels like a bug or limitation in the type inferrence.
The text was updated successfully, but these errors were encountered: