Skip to content
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

bad type inference #40856

Closed
trinarytree opened this issue Mar 3, 2020 · 3 comments
Closed

bad type inference #40856

trinarytree opened this issue Mar 3, 2020 · 3 comments
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. closed-not-planned Closed as we don't intend to take action on the reported issue

Comments

@trinarytree
Copy link

in the following program

import 'dart:async';

T max<T>(Iterable<T> items, Comparator<T> compare) => null;

class A {
  int x = 0;
}

Future<A> getA() async {
  A a;
  return a ?? max([A()], (a1, a2) => a1.x - a2.x);
}

clearly T ought to be bound to A, but the compiler doesn't do this. instead it tries to bind T to FutureOr<A>, which yields this error:

foo.dart:11:41: Error: The getter 'x' isn't defined for the class 'FutureOr<A>'.
 - 'FutureOr' is from 'dart:async'.
 - 'A' is from 'foo.dart'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'x'.
  return a ?? max([A()], (a1, a2) => a1.x - a2.x);
                                        ^

one way to work around this is

var m = max([A()], (a1, a2) => a1.x - a2.x);
return a ?? m;

but it's pretty counterintuitive why inlining an expression should cause a program to break. another workaround would be to explicitly bind T:

max<A>(...)

but why are these workarounds needed? part of the problem seems to be that in an async context, one is allowed to return a Future or a non-Future, which will automatically be wrapped in a Future. for some reason, this causes the compiler to gravitate toward guessing that the type of a returned value is a FutureOr, which makes using the dot operator on such a value nearly useless.

is there any way to make the compiler a bit smarter about these cases? e.g. here, it has at least 2 clues about how to bind T (the return type and the type of the param to max), one of which is ambiguous (the return type) and the other much less ambiguous (the type of the param to max). why doesn't it prefer to use the less ambiguous clue? another option would be to try both possibilities for binding T: A and Future<A>.

Dart VM version: 2.5.2 (Tue Oct 8 16:17:11 2019 +0200) on "macos_x64"

@keertip keertip added the area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. label Mar 3, 2020
@keertip
Copy link
Contributor

keertip commented Mar 3, 2020

@leafpetersen

@lrhn
Copy link
Member

lrhn commented Mar 3, 2020

Dart type inference does not work by general constraint solving. It uses an algorithm where context type information, if available, is pushed down into expressions, then the types of sub-expressions are combined to find type of the expression itself.

This case is equivalent to:

T max<T>(Iterable<T> items, Comparator<T> compare) => null;
FutureOr<A> returnValue = max([A()], (a1, a2) => a1.x - a2.x);

The type inference pushes the FutureOr<A> type down to the max call, which immediately becomes max<FutureOr<A>>([A()], (a1, s2) => a1.x - a2.x));. Then, knowing the type of K, that's it's pushed down further again, to infer

... = max<FutureOr<A>>(<FutureOr<A>>[A()], (FutureOr<A> a1, FutureOr<A> s2) => a1.x - a2.x));

Since FutureOr<A> does not have any x member, the code is incorrect.

The reason that

var m = max([A()], (a1, a2) => a1.x - a2.x);
return a ?? m;

works is that it infers

dynamic m = max<dynamic>(<A>[A()], (dynamic a1, dynamic a2) => a1.x - a2.x);

for you. With no context type, the parameters are inferred in a vacuum. The [A()] becomes <A>[A()] and the function, with no hints at all, infers dynamic for the parameters. Then the upwards inference combines the two possible values of K (A and dynamic) into the most general, dynamic.
If you use the "no implicit dynamic" lint, you would probably get a warning here.

(The choice to allow you to directly return futures in async functions made sense in Dart 1, but in Dart 2 it has become an inherited liability because of issues like this. If you had to return a value of type A, then the context type of the return statement expression could have been A, but because we allow you to return an A or a Future<A>, all we can restrict the expression to is a FutureOr<A>).

@lrhn
Copy link
Member

lrhn commented Oct 2, 2020

Working as intended, and not a library issue. We are aware of the shortcomings of the type inference algorithm, and are considering that in the language repository.

@lrhn lrhn closed this as completed Oct 2, 2020
@lrhn lrhn added the closed-not-planned Closed as we don't intend to take action on the reported issue label Oct 2, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. closed-not-planned Closed as we don't intend to take action on the reported issue
Projects
None yet
Development

No branches or pull requests

3 participants