Skip to content

Function literal return type inference fails on divergent future #3148

Open
@eernstg

Description

@eernstg

Consider the following program:

class Divergent<T> implements Future<Divergent<Divergent<T>>> {
  noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

void main() => () async => Divergent<int>();

This program is rejected by the CFE and the analyzer with similar error messages. Analyzer:

The return type 'Divergent<int>' isn't a 'Future<Divergent<Divergent<Divergent<int>>>>', as required by the closure's context.

CFE:

lib/main.dart:6:28:
Error: A value of type 'Divergent<int>' can't be returned from an async function with return type 'Future<Divergent<Divergent<Divergent<int>>>>'.
 - 'Divergent' is from 'package:dartpad_sample/main.dart' ('lib/main.dart').
 - 'Future' is from 'dart:async'.
void main() => () async => Divergent<int>();
                           ^

The return type inference for a function literal is specified here.

Checking the current definition of flatten we get flatten(Divergent<T>) == Divergent<Divergent<T>>.

For the inference of the return type of the function literal, we note that the imposed return type schema has no relevant information (and we get the same error with var _ = /*same function literal*/;), and also that the returned expression has type Divergent<int> as stated, which is then also the type of the returned expression after type inference for any context type because there's no thing for type inference to do.

So the actual returned type is flatten(Divergent<int>), that is, Divergent<Divergent<int>>. This implies that the inferred return type R for the function literal is Future<flatten(Divergent<Divergent<int>>)>, that is, R == Future<Divergent<Divergent<Divergent<int>>>>.

This shows that the implementations are computing the specified return type, and the only remaining problem is that the given returned expression does not satisfy the typing constraints associated with that return type.

So is it justified that we're returning a Divergent<int> in an async function whose return type is R == Future<Divergent<Divergent<Divergent<int>>>>? Should we get an error or not?

Let's explore some parts of the supertype graph of Divergent<int>, in order to see whether or not we can find a supertype which should be accepted. If this is true then Divergent<int> should arguably also be accepted.

By the declaration, Divergent<int> <: Future<Divergent<Divergent<int>>>. Other supertypes can be created by a rewrite operation that changes Divergent<T> to Future<Divergent<Divergent<T>>> for any T. No matter which steps we take, this will yield a type of the form Future^k<(Divergent|Future)^m<int>> for some k >0. Let S be the type we computed by any set of steps like this.

The test is that Future<flatten(S)> must be assignable to the return type R, that is, it must be a subtype of R. However, that is never true because the expansion steps described above will always create an S where Future is applied to the type argument at some level of nesting for any number of expansion steps greater than 1, and the types Divergent<int> and Future<Divergent<Divergent<int>>> (zero steps plus one step) are not subtypes of R.

The rules about inference of the return type of a function literal are surely intended to satisfy the sanity condition that each returned expression must satisfy the typing requirements for a returned expression, but that doesn't hold in this particular case.

In general, we don't want type inference (of any kind) to yield a program with type errors based on the failure to satisfy a subtype requirement where an inferred type is one of the operands. ("If we infer a type then it must be a type that works.")

However, the conclusion is probably the following:

  • It is now known that some typing situations can give rise to this kind of type error.
  • We could detect the situation and use an inferred return type of Future<Object?> or something like that.
  • We could also do nothing, and accept that it is possible to get this kind of error.

Note that, presumably, the class Divergent has a very unusual typing structure, and it is not obvious that anything with that structure would be useful or important to support.

@dart-lang/language-team, WDYT? Do we just keep everything as it is? Or do we use the return type Future<Object?> (or something like that) when this kind of type error occurs?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions