-
Notifications
You must be signed in to change notification settings - Fork 205
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 and await
.
#2710
Comments
Good question! 😄 Based on the most recent proposal about flatten, inline class N1 { // At run time: `N1` means `Future<int>`.
final Future<int> it;
N1(this.it);
}
void main() async {
N1 n1 = N1(Future.value(1)));
var x1 = await n1; // `x1` has static type `N1`, the future is not awaited.
} We should consider at least one more case: inline class N2 { // At run time: `N2` means `FutureOr<int>`.
final FutureOr<int> it;
N2(this.it);
}
void main() async {
N2 n2 = N2(Future.value(1)));
var x2 = await n2; // `x2` has static type `N2`, the future is not awaited.
}
Agreed, basically. However, it may seem somewhat surprising that we know statically that the representation object has type You could say that we're protecting the encapsulation of the representation object by refusing to take it into account during the computation of Maybe we should simply say that
This could be taken as a hint that we'd want to make it an error to await an inline type for now: Some/many of the inline classes whose representation type has a future type might be changed to have |
It's always hard to guess about intent, when the language and code doesn't allow the user to express any. If I did (as I'm likely to): // Like `Future`, but with superior typing of `onError` functions!
inline class BetterFuture<T> {
final Future<T> _realFuture;
BetterFuture(Future<T> future) : _realFuture = future;
BetterFuture<R> then<R>(FutureOr<R> Function(T) onValue,
{FutureOr<R> Function(Object, StackTrace)? onError}) =>
BetterFuture<R>(_realFuture.then(onValue, onError));
BetterFuture<T> catchError<E extends Object>(
FutureOr<T> Function(E, StackTrace) onError, [bool Function(E)? test]) =>
BetterFuture<T>(_realFuture.catchError(
(Object o, StackTrace s) => onError(o as E, s),
(Object o) => o is E && (test?.call(o) ?? true));
BetterFuture<T?> ignoreError<E extends Object>([bool Function(E)? test]) =>
BetterFuture<T?>(_realFuture.then<T?>((v) => v,
(Object o, StackTrace s) => (o is E && (test?.call(o) ?? true)) ? null : (throw o);
} I might actually expect to be able to We cannot see that if the author cannot state that the type should be treated as a As you suggest, we can just disallow awaiting an inline class type. It's inconsistent with what we otherwise do for non-future types, but if the argument is that we can't decide whether it's a non-future type at all, then failing might be the better option. It does suggest that we'd definitely want the "implement interface" eventually, so that it's possible to implement (Which might be an argument for allowing the feature to implement interfaces from the start, because without that, the feature doesn't cover all the desired uses out of the box, and there is no real workaround that won't require rewriting later, if/when we do get the feature.) Nit: As usual
doesn't work, because everything is a subtype of |
|
The feature specification of 'extension types' makes it a compile-time error to await an expression whose type is an extension type |
We probably need to consider union types too.
It's also a problem that That may be to not introduce an implicit await when returning an extension type on an async function, unless the extension type implements Again, being an extension type is not enough, have to remove |
I don't think we need to disallow awakening extension types that do not implement future. |
The most permissive approach would be to consider This would be sound, and it would yield the best possible typing of I'd prefer to maintain some amount of encapsulation. So if an extension type It's a bit like making The underlying rationale for being even more strict with extension types than we are with class types is that extension types are compile-time-only types, and hence it's an unrecoverable loss of information to cast away the extension type. Say, if you have an I agree that we should include union types as well:
|
The "union free type" would ignore the I also worry about disallowing await on an extension type, and not on |
Questions for which we don't have clear answers at this time:
We could specify that it is a compile-time error at
|
Noting the status today, Jan 9 2024. Consider the following program: extension type F<X>(Future<X> it) implements Future<X> {}
void main() async {
F<int>? fut = F<int>(Future<int>.value(42)) as dynamic;
await fut;
} The common front end and the analyzer disagree on the treatment of this program (according to DartPad based on Dart SDK 3.3.0-273.0.dev). The CFE accepts the program, but the analyzer rejects it as follows:
The extension type feature specification does indeed say that
However, However, it seems inconsistent to accept We don't have a type that corresponds to |
The flatten function is the gift that keeps giving. Then we try to give rules about when you can and cannot The value of flatten( The rule that you cannot await an extension type which does not implement That is, rules in priority order:
Then
|
This is a very radical change of the definition of flatten, I think it would be a safer bet to start from the current definition and then add rules as needed to handle extension types. Note that the current rules already handle extension types in the several ways (where
|
The current definition of flatten and derived future type may need a little tweaking. The definition is:
I think the "future type derived from" should be defined inductively, instead of trying to list cases. We cannot assume that a Dart type is not of the form At least For We have a discrepancy between CFE and analyzer on this code, where flatten( Another case that worries me is This is So I'd consider rewriting the "future type derived from" iteratively as:
(We can also make it normalize the About flatten, I find it hard to read, because it's so indirectly defined in terms of other functions and relations that are hard to remember the details of, but it's probably correct with the above change to "derived future type", and accounting for the derived future type being able to have multiple I think we can collapse the two "derived future type" cases into:
Maybe it can just be:
but that's really more like:
|
About a minimal change to specify the "can't await an extension type that doesn't implement
and it's a compile time error to These are the places (if I remembered them all) where, if S derives a future type, that's the future type that flatten would flatten, |
That's great! I think we'd want the following adjustment (motivation below):
For this rule, we'd want the property that flatten will never be applied to a type which is incompatible with await. In order to see that this is indeed true, we need this lemma: Lemma: If Proof sketch: Case: Case: Case: Case: Apart from the need to ensure that flatten is well-refined, another reason why I included the rule about types of the form import 'dart:async';
// --- Standard static type helper.
typedef Exactly<X> = X Function(X);
extension<X> on X {
X expectStaticType<Y extends Exactly<X>>() => this;
}
// --- Example .
// Does not derive a future type.
extension type E(Future<int> _) implements Object {}
void test<
X extends FutureOr<Object>, // Derives `FutureOr<Object>`.
XQ extends FutureOr<Object>? // Derives `FutureOr<Object>?`.
>(
X x,
XQ xq,
) async {
// Without promotion, `x` and `xq` _do_ derive a future type.
(await x).expectStaticType<Exactly<Object>>;
(await xq).expectStaticType<Exactly<Object?>>;
// With promotion, we should still be able to see that future type.
if (x is E) {
(await x).expectStaticType<Exactly<Object>>;
}
if (xq is E) {
(await xq).expectStaticType<Exactly<Object?>>;
}
}
void main() {
var future = Future.value(1);
test<FutureOr<Object>, FutureOr<Object>?>(future, future);
} |
I created a proposed feature spec update in #3560. |
The first line won't work. An extension type which implements So maybe:
Here " The third line is probably sound. It assumes that we can't have a "
Which means that items 2 and 3 throw away type variables to get the promoted type or bound, except that if the promoted type doesn't derive a future type, we throw that away and continue on the type variable('s bound) instead. Do we need to swap item 2 and 3, otherwise Example: foo<X extends Object?>(X v) {
if (v is ExtensionTypeNotFuture?) {
await v;
}
} This would promote If we use item 3, then we ignore That's different behavior from
We should be a little careful, because for Currently it's not possible to promote a type variable which implies a future type to a subtype which does not (except possibly |
So, coming back to it, I think the definition I'd go with is:
This should cover all the cases where a user could be confused if might they think they can await an extension type with a representation type which implements So a type is incompatible with await if changing whether awaiting an extension type will await the representation type, would make a difference. (Which excludes the cases where the extension type does implement I find it easier to convince myself that it actually covers all the cases, than when defined using the transitive "implements" and " |
Great comments, thanks! ;-)
Yes, an extension type could implement a lot of other extension types or non-extension types, directly or indirectly.
That's better. Using that.
That's not quite true, 'implements' includes type variables whose bound (or bound's bound, etc.) has a superinterface path as specified.
Right, I gave a proof sketch here. I think this is a property that we want, and it shouldn't be too hard to ensure that it always holds.
The criteria are specified to be used symmetrically ("at least one holds"), so the ordering should not matter. However, it looks like it would be a really good idea to double-check that. ;-) I changed the rules (#3560) such that 'being
Assume the following: extension type N(Future<int> _) {}
extension type N2(Future<int> _) implements N {}
extension type F(Future<int> _) implements Future<int> {}
extension type F2(Future<int> _) implements F {}
void test<
X extends Object?,
XN extends N,
XF extends F,
XXN extends XN,
XXF extends XF,
XNQ extends N?,
XFQ extends F?,
XXNQQ extends XNQ?,
XXFQQ extends XFQ?
>(X x, XN xn, XF xf, XXN xxn, XXF xxf, XNQ xnq, XFQ xfq, XXNQQ xxnqq, XXFQQ xxfqq) {
... // await various expressions.
} We had a number of examples, they all work out. For example, the following types are compatible with await: Incompatible: Intersection types were a bit twisted, so let's take them separately:
I believe that's been fixed now.
If we wish to do that then we can just turn the intersection type item into something like this:
We would then make the choice that it is an error to await a promoted type even though the corresponding non-promoted type can be awaited. This is unusual, but it does align kind of nicely with the fact that Please note, though, that the current rule has the nice structural property that it is the least strict rule (not in a very strict formal sense, but I think it's a reasonable characterization) which ensures that flatten is never invoked on a type that isn't compatible with await. |
(OK, so we commented simultaneously. But we agree on most of it, so that's kind of consistent. ;-) |
Right, the only difference between the two proposed rules is that your proposal contains this one:
which basically means that we will make it an error to That would be fine with me, too. |
Updated #3560 accordingly. |
Landed #3560. |
I'm sorry for commenting an old post, but I see this can be related to my question: the keywords async and await convert FutureOr into asynchronous even if it can be synchronous code. |
I think your comment raises an issue which isn't closely related to the topic of this issue. Please just create a new issue. Someone may respond quickly, otherwise I'll take a look when I'm back from lunch. ;-) |
(@elkSal, will you create that new issue? It's a bit messy if we have substantial discussions about topic B in an issue about topic A; and I do think these topics are different; and this one is closed, anyway. ;-) |
(NOTICE: Written when the feature was called "inline class", is now "extension type", and is not completely the same. New conclusion too, probably, so check comments.)
TL;DR: flatten(N) of an inline class type N should be N.
What happens if you do
await e
and the static type ofe
is an inline class, N?The runtime semantics are pretty clear, so it boils down to defining what flatten returns on an inline class.
(And it's probably going to be trivial, but let's make sure it's documented.)
As currently defined, an inline class can only implement other inline classes, so it cannot implement
Future
(orFutureOr
).It would seem valid to make flatten(N) = N for an inline-class type N, like we do for other types which do not imply an expected
Future
type.At runtime, the inline class type does not exist, it's replaced entirely by the representation type.
(Basically, at runtime the inline class type can be treated as a type alias for the representation type, instead of a separate type. It needs to have all the same cyclic-reference restrictions as a type alias to avoid an infinite expansion when going from the static type to the runtime type.)
If N has representation type R, the static type of
await e
will be flatten(N), aka N, which is the same as R at runtime.The
await
needs to do anis Future<R>
check on the value ofe
to see if it should be, and can soundly be, awaited.If the representation type is actually
Future<int>
itself, we won't await that future, because it's not aFuture<Future<int>>
.That should work, and is probably what we want.
An alternative could be to looks at the representation type immediately, and see if it mentions a future type, and if so assume that we are awaiting that type.
I don't think it's a good idea. Mainly because it breaks the abstraction of the inline class, but also because it opens up questions about where R being a Future<R> should make the
await e
switch back to being typed as N or not.So, let's not do that, the flatten(N) = N is simpler.
If, at a later time, we allow an inline class type to implement interfaces or other types (which must then also be supertypess of the representation type), then it may become possible for an inline class type to implement
Future
orFutureOr
. At that point we should look at the future-behavior of the type itself, just as we do for other types. That should be sound wrt. the representation type, since it requires the representation type itself to have those types.The text was updated successfully, but these errors were encountered: