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

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

Open
eernstg opened this issue Feb 2, 2024 · 3 comments
Labels
extension-types small-feature A small feature which is relatively cheap to implement.

Comments

@eernstg
Copy link
Member

eernstg commented Feb 2, 2024

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

@eernstg eernstg added small-feature A small feature which is relatively cheap to implement. extension-types labels Feb 2, 2024
@lrhn
Copy link
Member

lrhn commented Feb 3, 2024

See also item 2 of #3257 (comment)

@eernstg
Copy link
Member Author

eernstg commented Feb 4, 2024

Oh, I actually forgot that we got so close to the same thing in #3257. You could say that this is just a follow-up which was already mentioned in the discussion of that issue:

If we wish to generalize the rules in the future along the lines discussed here then we can handle that in a new issue.

I think it's worth revisiting this idea now. Over the past few months (#3257 was written in August 2023) I've tried out various usages of extension types, and the fact that we can't have something like SafeFuture<int> f() async {...} sticks out as an unnecessary restriction. (And the obvious workaround has a run-time performance cost, not just a developer inconvenience cost.)

Also note that we support switch (myFuture) { case SafeFuture(): ... } , which is yet another way to achieve a typing of a given future as an extension type without running a constructor. In that context we will have a lint to warn developers in the case where SafeFuture doesn't implement Future, which is an even more permissive approach than the one which is being proposed for returns here.

@eernstg
Copy link
Member Author

eernstg commented Feb 14, 2024

Note that the proposal in #3614 would make this proposal more like a special case of a more general feature (details given there).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension-types small-feature A small feature which is relatively cheap to implement.
Projects
None yet
Development

No branches or pull requests

2 participants