-
Notifications
You must be signed in to change notification settings - Fork 217
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 asFuture<T>
, whereT
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 asIterable<T>
, whereT
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 asStream<T>
, whereT
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