dart-lang / dart-lang/language
[extension-types] Allow implicit return mechanisms to use an extension type as if it were the representation type
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
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`, 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`, 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`, 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`.
**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:
```dart
extension type E1(int _) {}
extension type E2(Future _) {}
extension type E3(Future _) implements Future {}
E1 f1() async {...} // Error, the erasure `int` does not satisfy the constraint.
E2 f2() async {...} // Error, `E2` does not implement `Future`.
E3> f3(X x) async {...} // OK.
```
Here is an example where this little snippet of expressive power could be helpful:
```dart
import 'dart:async';
import 'dart:math';
extension type SafeFuture(Future _it) implements Future {
SafeFuture then(
FutureOr onValue(T value), {
T Function(Object, StackTrace)? onError,
}) =>
SafeFuture(_it.then(onValue, onError: onError));
}
Future f1() async {
// ... async stuff ...
return Future.error("Failed Future!");
}
SafeFuture 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` with static type `Future` 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:
```dart
// Workaround which is available today.
SafeFuture f2() {
Future 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
Contributor guide
Research direction
Start by reviewing the implicit async, sync* and async* return constraints together with extension type erasure and superinterface rules described in the issue. Use the E1, E2 and E3 examples as the initial cases; done means the accepted language specification permits the E3-style return while continuing to reject E1 and E2.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100