Skip to content

[extension-types] Allow implicit return mechanisms to use an extension type as if it were the representation type #3607

@eernstg

Description

@eernstg

Certain kinds of function have a return mechanism which is implicit:

  • A function whose body has the modifier async will return an object typable as Future<T>, where T is the future value type of the function (which is computed based on the declared return type).
  • A function whose body has the modifier sync* will return an object typable as Iterable<T>, where T is the element type of the function (computed from the declared return type).
  • A function whose body has the modifier async* will return an object typable as Stream<T>, where T is the element type of the function (computed from the declared return type).

The given future/iterable/stream is created as part of the built-in semantics of those functions, and the developer who writes such a function doesn't have an opportunity to specify the kind of object which is being returned.

Consequently, it is a compile-time error for such functions to have a return type that fails to satisfy a certain constraint. For example, it is an error for T f() async {...} if T isn't a supertype of Future<Never>.

This issue is a proposal that we also allow such functions to have a return type which is an extension type that implements a type that satisfies the constraint, and whose extension type erasure also satisfies the constraint. The future value type resp. element type of the function is computed from the extension type erasure.

In other words, everything is unchanged, except that we're also allowed to return an extension type whose erasure is OK, if it also "admits" to being a future (or iterable, or stream) by having a Future (Iterable, Stream) type as a superinterface. For example:

extension type E1(int _) {}
extension type E2<X>(Future<X> _) {}
extension type E3<X>(Future<X> _) implements Future<void> {}

E1 f1() async {...} // Error, the erasure `int` does not satisfy the constraint.
E2<int> f2() async {...} // Error, `E2` does not implement `Future`.
E3<List<X>> f3<X>(X x) async {...} // OK.

Here is an example where this little snippet of expressive power could be helpful:

import 'dart:async';
import 'dart:math';

extension type SafeFuture<T>(Future<T> _it) implements Future<T> {
  SafeFuture<R> then<R>(
    FutureOr<R> onValue(T value), {
    T Function(Object, StackTrace)? onError,
  }) =>
      SafeFuture(_it.then(onValue, onError: onError));
}

Future<int> f1() async {
  // ... async stuff ...
  return Future.error("Failed Future!");
}

SafeFuture<int> f2() async { // Currently a compile-time error, will be OK.
  // ... async stuff ...
  return Future.error("Failed SafeFuture!");
}

void main() async {
  if (Random().nextBool()) {
    var fut = f1();
    print('Using a regular Future');
    await fut.then((_) => 42, onError: print); // No error, but throws.
  } else {
    var sfut = f2();
    int i;
    print('Using a SafeFuture');
    // await sfut.then((_) => 42, onError: print); // Compile-time error.
    i = await sfut.then((_) => 42, onError: (o, s) => 24); // OK!
    print('Done, got $i');
  }
}

The point is that a SafeFuture requires a fully typed onError in the signature of its then method, which will eliminate the run-time type error that the regular Future incurs.

(OK, we might want a then and a thenNoStackTrace to allow an onError that doesn't receive the stack trace, but that's just something we can play around with, the point is that we can modify and/or add members to Future, and we can use that enhanced interface of futures all over the place because all async functions can return it, if we wish to do that.)

A similar technique could also be used to provide an invariant future type (EvenSaferFuture ;-), which is needed in order to avoid a covariance related run-time type error (e.g., if we have a Future<int> with static type Future<num> and pass an onError that has return type num).

We can just go ahead and do this in most cases, but when it comes to async functions it creates a need for an inconvenient wrapper function:

// Workaround which is available today.
SafeFuture<int> f2() {
  Future<int> inner() async {
    // ... async stuff ...
    return Future.error("Failed SafeFuture!");
  }
  return SafeFuture(inner());
}

This proposal allows us to avoid this wrapper function which saves developer time and execution time. It is type safe, because the return statements will rely on the representation type of the return type which is actually what we have at run time

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-types-laterIssues about extension types for later considerationsmall-featureA small feature which is relatively cheap to implement.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions