One thing we’ve left as an unresolved question so far in the matter of async/await syntax is the exact final syntax for the await operation. In the current implementation, awaits are written using a compiler plugin:
async fn foo() {
await!(bar());
}
This is not because of any technical limitation: the reason we have done this
is that we have not decided on the precise, final syntax for the await
operation. However, no one really wants to stabilize on the current syntax, so
some forward progress in this area would be beneficial for us.
I’m proposing in this post that in the near future (😏) we introduce await as a
keyword with mandatory delimiters (similar to unsafe
) as a more lasting
syntax which is forward compatible with other alternatives as well.
The problem of precedence
The reason we currently use the compiler plugin syntax is that await
and ?
have a tricky problem involving operator precendence. Consider this code:
let response = await http::get(url)?;
The return type of the http::get
function is almost certainly something with
the same shape as impl Future<Output = io::Result<Response>>
. That is, the
function returns a future that will eventually either succeed or return an
error. What this means is that what you want the behavior to be is this:
- First, await the future, evaluating it to its output
(
io::Result<Response>
). - Second, use
?
to unwrap the result, early returning with the error if it wasn’t successful.
What this means is that you want await to bind tighter than ?
- you want
the ?
operatior to be applied to await http::get(url)
, not the other way
around. But when you look at that code, it would be fair to assume that things
work the other way around - ?
is almost always written without a preceding
space, which certainly suggests that it binds tighter than await
.
And this is a representative example. Any future that operates over IO (which is the main reason async/await exists) will probably return some kind of error, and its fairly rare to return a result while setting up a future, before it begins evaluating. What this means is that there is a conflict between the useful order of precedence and the obvious one.
Potential solutions to precedence problems
There are several possible ways to solve this problem, and we settled on the
await!()
syntax to avoid having to pick one:
- Pick the useful precedence. If users want the uncommon order of operations,
they would write:
await (function()?)
, and the default precedence might surprise someone reading asynchronous code for the first time (though they would hopefully pick it up quickly since it is probably commonly used). - Pick the obvious precedence. Whenever users want to await and then
?
, they will have to use parens:(await function())?
. This keeps the code from ever surprising anyone, but at a pretty steep cost: users will be writing this weird parens code all the time. - Pick the obvious precendence, but introduce a new syntax sugar to avoid the
weirdly parenthesized code. For example,
await? function()
: by putting the?
onawait
, that is a special combined await-then-?
operation. - Introduce some kind of postfix syntax instead of prefix
await
. A postfix syntax would never have any ambiguity about precedence. The problem is that such a syntax would be very exotic and no one has come up with a proposal for a particular syntax that was widely popular. - Always require delimeters on
await
. This way it will always be clear that what you are throwing, because a?
inside the braces throws before the await and a?
outside the braces throws after.
The delimiter solution is forward compatible
Shipping async/await is high priority, so we need to find a pragmatic solution in the near future. This led me to ask the question: are any of these possible syntaxes forward compatible with the other solutions? In my opinion, the final solution - require delimiters - is very strongly forward compatible with other options people might prefer.
Forward compatibility with useful precedence
Entirely forward compatible
This is the most straight-forward forward compatibility (😏) of all of them. If
we were decide on using the useful precedence, await { e }?
would just be the
composition of a await
, a block, and ?
, having exactly the same meaning it
has under the required delimiter. All that would change would be that the
braces would sometimes become unnecessary.
Forward compatibility with obvious precedence
Mostly forward compatible
This is the one which is least straightforward. Under the most grammatically
orthogonal implementation of the obvious precedence solution, await { e }?
would have to mean that ?
was applied before await
, changing its meaning
and making the delimiter solution not forward compatible.
However, required delimiters are forward compatible with applying the obvious
precedence with the sole exception of braces. In that case, await { e }
would
not be interpreted the composition of await
and a block, but its own
syntactic variation on await
which has the useful precedence. This is not so
different from the fact that if true { e }
is not actually the composition of
if true
and a block, but a brace-using construction of its own. I think, if
we decided we wanted await e?
to have the obvious precedence, we could easily
afford this small irregularity in our grammar around blocks, which I don’t
think would actually surprise users any more than the difference between blocks
and brace-using control flow already surprises them.
Forward compatibility with postfix syntax
Entirely forward compatible
I’m not personally very enthusiastic about postfix solutions to await, because
there hasn’t been a proposal that doesn’t have a really big problem in my
opinion. But even if we were to someday have a viable proposal for a postfix
await syntax, in my opinion, having a syntax that looks more like the syntax
adopted by every other async/await language would still be valuable as an
onboarding “easiness” feature. That is, even if its idiomatic to write
something like foo()@?
instead of await { foo() } ?
, having the latter
syntax as a synonym helps people coming from languages onboard into Rust.
So yes, I think adding await { foo() }
is forward compatible with someday
having a postfix syntax as well.
Conclusion
Everyone has their own preference regarding their ideal, favorite syntax, but
as a pragmatic step toward stabilization in light of the existing differing
opinions, I don’t think there’s any proposal nearly as solid as moving to
delimiter-required await
. That’s why, I’d propose that we decide to stabilize
that syntax, with the possibility of future extension to one of the other
alternatives someday later. (It’s also possible, of course, that we never
extend it, and delimiters stay mandatory forever!)
The only wiggle in this is that the current macro syntax is being used by the tokio-async-await bridge to define its own macro, which is able to await futures from 0.1. Concurrent with this post, I’ve been laying the groundwork for getting futures 0.3 to run on stable with the necessary prerequisite stabilizations. I think we should be cautious about implementing this change and disrupting that bridge until we’ve gotten closer to stabilizing futures 0.3 (which I’m hoping we can accomplish no later than version 1.33 at the beginning of March 2019).