language
language copied to clipboard
Add an await pattern?
Should we add a pattern form which expects a Future
and awaits
it? e.g.
void test(Map<String, Future<int>>? e) async {
switch (await e) {
case {"first" : await x, "second" : await y}?: ...
case null: ...
default : ...
}
This seems very useful for destructuring data structures with asynchronous components.
cc @munificent @eernstg @lrhn @natebosch @jakemac53 @stereotype441
I haven't thought through this deeply, but I think I would propose the semantics that await p
matches if the value being matched is a Future
, and the result that you get from awaiting it matches p
.
We could instead though remove the check for Future
, to make it more consistent with normal await
: that is, await p
matches v
if v
is a Future and the result of awaiting it matches p
or of v
is not a Future
and v
matches p
.
Thoughts?
We could instead though remove the check for
Future
, to make it more consistent with normalawait
: that is,await p
matchesv
ifv
is a Future and the result of awaiting it matchesp
or ofv
is not aFuture
andv
matchesp
.
I like where you're going with this. Making the semantics more consistent with normal await
seems right to me, partly because I think it's more likely to be consistent with user expectations, and partly because it makes the lowering simpler. For example, for any type T
, I would like for the lowering of:
case await (T t): ...
to be equivalent to:
var tmp = await x;
if (tmp is T) {
...
}
But this doesn't actually work for the above definition, because if the runtime value of v
is a Future yielding an int, and T
is Future<dynamic>
, then by the above definition, the match should succeed, but by this desugaring it fails.
I would rather just specify:
await p
matchesv
if the result of evaluatingawait v
matchesp
.
(Maybe with some non-normative accompanying text explaining that, except in wacky corner cases, this implies that the match will succeed if v
matches p
).
That way it's unambiguously clear that desugaring into a simple await
is correct.
And of course we'll want to remember to write language tests that exercise the wacky corner cases 😄
I like this idea, though I don't know if it's important enough to slot into the initial release.
I would make it consistent with normal await
: an await
pattern matches if its subpattern matches. This is value because it means that the await
pattern is irrefutable if the subpattern is, which lets you use it in irrefutable contexts:
void test((Future<int>, Future<String>) futures) async {
var (await age, await name) = futures;
}
The more interesting question is whether all the await
patterns in run in parallel or sequentially. Parallel would be really nice but since the other patterns can invoke user-defined code, specifying the behavior precisely might be tricky.
The problem with an await pattern is that it must await a future before knowing whether the pattern matches.
If I do switch (someObject as Object?) { case await int x: .. }
, what would happen if the value is:
- an integer? Perform an await on the integer, wait a microtask, the accept.
- a string? Perform an await on the string, wait a microtask, reject and move on
- a
Future<Object>
completing with anint
? Wait for future to complete, then accept. - a
Future<Object>
completing with aString
? Wait for future to complete, then reject. - a
Future<Object>
completing with an error? Wait for future to complete, then throw.
Or should await p
reject if the matchee is not a Future
?
If I do
case await int x: ...
case await String x: ...
will the future be awaited twice (two microtasks) if it's not an int
, or will we cache the result of awaiting, like we cache the result of calling getters? (We should, there should only ever be one call to then
of each matchee in the entire switch.)
f I do
switch (someObject as Object?) { case await int x: .. }
, what would happen if the value is:
- an integer? Perform an await on the integer, wait a microtask, the accept.
Yes, same as an await
expression.
- a string? Perform an await on the string, wait a microtask, reject and move on
Yes, same as an await
expression.
- a
Future<Object>
completing with anint
? Wait for future to complete, then accept.
Yes, same as an await
expression.
- a
Future<Object>
completing with aString
? Wait for future to complete, then reject.
Yes, same as an await
expression and then apply that value to the inner subpattern, which fails to match.
- a
Future<Object>
completing with an error? Wait for future to complete, then throw.
Yes, same as an await
expression.
Or should
await p
reject if the matchee is not aFuture
?
I think it should match since await
expressions permit non-Future values.
If I do
case await int x: ... case await String x: ...
will the future be awaited twice (two microtasks) if it's not an
int
, or will we cache the result of awaiting, like we cache the result of calling getters? (We should, there should only ever be one call tothen
of each matchee in the entire switch.)
It will cache. Basically any "operation" (getter call, field access, []
operator, await
, etc.) performed on a "known object" (the matched value, or the result of any previous operation stemming from a known object) is cached.
Doing the await only if a pattern has not been rejected yet means that optimizations can change timing:
case (await var x, int y) = pair;
can be optimized to check the second field first, and if that's not an int
, the first field's future is not awaited.
We should be very clear about whether such optimizations are allowed (they should be), especially if a prior case had been case (String x, num y)
and that had failed due to y
not being a num
, then a good compiler can skip right past any check that is impossible due to prior checks.
(And as usual, where behavior is unspecified, I'd like for our dev-mode compilers to add some randomness, so people don't start assuming behavior is consistent and dependable.)
In general, pattern matching can have user visible side effects, since many kinds of patterns end up invoking methods that could be user-defined. And, yes, if we want to allow the compiler to reorder stuff, then it means we have to explicitly say that the order is unspecified and users can't rely on it.
I'm interested in this but given how big the pattern matching feature is already, I'm going to move it to "patterns-later".