dart-lang / language

Design of the Dart language
Other
2.65k stars 201 forks source link

Bad type inference when chaining generic anonymous functions #2037

Open mat100payette opened 2 years ago

mat100payette commented 2 years ago

Here is some function that chains multiple generic functions and returns the result of composing these functions on the caller:

extension ChainExtension<T> on T {
  S chain<R, S>(
    R Function(T value) f,
    S Function(R value) g,
  ) {
    return g(f(this));
  }
}

The problem arises when you try to call it with anonymous functions:

int res = 123.chain(
  (foo) => foo.toString(),
  (bar) => int.parse(bar), // Error
);

In this scenario above, the compiler will know that foo is an int. However, it will not infer String for bar; instead, it will infer Object?, resulting in a compile-time error on int.parse(bar).

Now, look at these scenarios:

  1. This compiles, and produces the correct output.
    int res = 123.chain(
    (foo) => foo.toString(),
    (String bar) => int.parse(bar),
    );
  2. This gives you a (wrong) warning that the cast is unnecessary:
    int res = 123.chain(
    (foo) => foo.toString(),
    ((bar) => int.parse(bar)) as int Function(String), // "Unnecessary Cast" warning here
    );
  3. This gives you the following compile time error:
    int res = 123.chain(
    (foo) => foo.toString(),
    (bool bar) => int.parse(bar), // Error
    );

    The argument type 'int Function(bool)' can't be assigned to the parameter type 'int Function(String)'.

So basically, the information that bar should be a String does exist at compile time; it is simply not conveyed to the type inference. This forces the code to be unnecessarily verbose by always adding the explicit types.

leafpetersen commented 2 years ago

cc @stereotype441 I think this may be an example that would benefit from the extended variant of the function literal heuristic you were exploring, right?

mat100payette commented 2 years ago

On a related note, yesterday I have also come across a similar inference issue. Please do tell me if it warrants a seperate issue.


Here is some common Flutter code where a typed Future<T> is initialized and then used in a FutureBuilder<T>:

Future<int> myFuture = Future.value(1);

FutureBuilder(
  future: myFuture,
  builder: (_, snapshot) => Text('Is my int even: ${snapshot.data?.isEven}'), // Error
);

This code raises a compile time error since snapshot is inferred to be an AsyncSnapshot<Object?>, as opposed to a AsyncSnapshot<int?>. Obviously, one could fix this error similarly by either doing:

FutureBuilder<int>(...)

or

(_, AsyncSnapshot<int?> snapshot) => ...

both of which aren't DRY.

Just like the chaining issue however, what's unintuitive for new people is that if you hover over FutureBuilder in the error snippet above, it'll tell you this:

(new) FutureBuilder<int> FutureBuilder({
  Key? key,
  Future<int>? future,
  int? initialData,
  required Widget Function(BuildContext, AsyncSnapshot<int>) builder,
})

which shows, here too, that the compiler already should know all the type information based on the usage of myFuture.

leafpetersen commented 2 years ago

The latter is definitely an instance of https://github.com/dart-lang/language/issues/731 , which @stereotype441 is investigating.

mat100payette commented 2 years ago

The latter is definitely an instance of #731 , which @stereotype441 is investigating.

Yes ok thank you, I wasn't 100% sure.

mat100payette commented 2 years ago

Would @stereotype441 mind confirming that the investigation is indeed going to fix this? Kinda left in the dark.

stereotype441 commented 2 years ago

@mat100payette said:

Would @stereotype441 mind confirming that the investigation is indeed going to fix this? Kinda left in the dark.

Yes, I can confirm that the issue you mentioned in https://github.com/dart-lang/language/issues/2037#issuecomment-1000491524 is most definitely the same issue as in #731, which I am working on this quarter.