Await Syntax Discussion Summary

Speaking from some experience with async/await in C#, my 2 cents on a prefix syntax:

  • Things like (await Foo()).Bar() are reasonably common and somewhat annoying, but you don’t run into that case all the time. There’s also some argument to be made that the forced parentheses highlight the actual control flow.

  • I agree that (await foo())? is too unergonomical for this very common usecase. In C#, all futures are the equivalent of Result<T, Exception>. You can query that by hand, but await automatically unwraps and rethrows the exception (which 99% of the time is what you want). An await? prefix would mostly solve this issue.

  • I don’t remember ever needing more complex chains like await (await Foo()).Bar() or anything similar, though I’m sure there are some cases.

Regarding a postfix syntax:

  • This surprised me given my C# experience, but I find the rationale for a postfix syntax quite convincing, especially because of how well it interacts with ?.

  • foo.await and foo.await() however is just an insidious trap for Rust beginners, if you don’t know what’s going on, nothing about this construct suggests that you’re not looking at a normal field access and instead need to go look for a tutorial. This could be an insane gotcha.

  • That the feature is “called” async/await is IMHO a very weak argument for having an await keyword, Rust isn’t Cobol.

Someone pointed out that with new and unfamiliar features, people tend to err on the side of verbosity. C# is full of examples here (it still has a delegate keyword as an earlier version of what’s now the lambda arrow). People will probably adapt to foo()#? or whatever very quickly.

Of course, taking that heuristic too far and you end up with Perl, so it’s definitely a hard thing to balance.

19 Likes

From my quick glance, the reasons it can't be a macro are for better error messages and to prevent abuse? I feel I'm missing something because that seems somewhat weak of an argument.

I'm really liking the idea I saw on reddit for UFCS for macros. Instead of creating a special way to declare postfix macros, allow any macro to be postfix.

let bar = "Hello {}".format!("world");
let bar = foo().async!()?;
let bar = foo().yield!()?;
let bar = foo().r#try!();
let bar = foo().dbg!()?;  // EDIT Someone else posted this other good example

I assume it isn't an edition-level breaking change to take a keyword and unreserve it if we decide macros can work and postfix macros solve the problems.

12 Likes

Closing this topic for 16 hours to apply backpressure to the discussion. Sorry everyone, but 85 comments in 15 hours is a lot.

Also, everyone who wants to participate in this discussion would be investing an hour well by watching this video:

(and my written response)

27 Likes

This topic was automatically opened after 16 hours.

For everyone following along, if you want to register your approval or lack there of to various existing alternatives without having to spam the thread, you can add your feedback to this straw poll.

If you would like to see the results or discuss the survey itself, there is a separate thread here. Results will be posted there.

2 Likes

How about

let x = future()!?;

We have ? for errors why can’t we have ! for await.

let x = future()!?.call()?.f()!?;

println("{}", future()!);

macro!(future()!);

This looks perfectly fine to me.

1 Like

It occurred to me that most of the issues with prefix-await, as well as a few issues with ? (namely, the interaction of it with control flow constructs if every branch of them would be returning a Result), would be fixed with an alternate, rather out-of-scope change: adding a prefix version of ?.

Using try as a placeholder for this:

try await ... = (await ...)?
await try .... = await (...?)

try match ... { ... } = (match... { ... })? //The 'control flow' issue for ?

In this situation a sigil version of await could still be added, much like ?, in the event it turns out that constructs like (await foo).bar are common enough to justify it.

Edit: Essentially, it comes down to the following ‘rules’:

  1. There should always be a prefix version of any special syntax, as it composes better with most of the language
  2. In the event that something happens frequently enough in the part of the language it doesn’t compose with (method chaining, mostly), an option that does can be added as an addition to the prefix syntax.

? is essentially the only thing currently in the language that breaks this, therefore it’s most valuable to fix that, rather than contort other additions to the language around the weirdness of ?

Having both try and ? doing the same thing in the language is far from ideal. If try were introduced, postfix ? should be deprecated and removed if there is ever a major version bump. That’s also far from ideal. I don’t think adding a try keyword is a viable solution.

Some thoughts on the potential .await!() syntax:

If there’s any chance of rust including a postfix macro facility in the future, that syntax should be preferred to .await!, .await and future await. If postfix macros materialize in the future, then choosing .await!() today will mean that tomorrow’s Rust has a smaller syntax surface area. If instead it becomes clear in the future that there will never be postfix macros, a .await!() syntax can be declared deprecated (but still supported) and replaced with something cleaner. I expect that a transition from .await!() to .await! would go much better than the other way around. The difference between this syntax change and replacing postfix ? with prefix try is that .await! is a cleaner and more concise syntax than .await!() in all cases, and it’s a change I expect most programmers would happily accept.

However, if there’s no chance of postfix macros ever becoming a thing, then .await!() should be avoided. The parentheses communicate that this is a syntactic construct that sees use beyond just await — why else would the await syntax include unnecessary parens?

Unrelated: I think a future await syntax would be more confusing than future.await. Consider return future await;.

3 Likes

The point is that it would not be a new feature, but a new syntax for a feature already working that would deprecate the previous one, shortly after it is released. The previous one should stay maintained because the Rust is committing on stability.

Technically, it is not possible since it would conflict with macro syntax. Visually, since the exclamation point is associated with risk in common pictograms, and it is already used for macros and negation, I believe it would be a puzzling sigil for the await syntax.

In my opinion, postfix macro syntax that expands to await {future} would be the best of both worlds.

If postfix macros become a thing, that could open the door for some interesting control flow, such as input.get().unwrap_or_retry!().

3 Likes

I don’t think you need to distinguish to hard between .await!() and .await!. I would assume if the postfix macro style ever gets into Rust there would be the possibility to have optional parentheses for macros with only one parameter. .await!() and .await! would be equivalent. Just in the case where you have more than one parameter these are mandatory. Think of format: "I am {} years old".format!(42); The thing left of the dot IS the first parameter so the parentheses for single argument postfix macros like future.await!() are useless anyway its just reordering of await!(future)

3 Likes

Personally I also deplore the sigil solution was dismissed. It seemed to me the cleaner way to go for a postfix syntax. Proposals like @await seem to me like a unnecessarily complicated version of the sigil.

Keyword as properties are really disturbing. It exist in Java (.class and .length), it’s surprising at first sight but it’s acceptable since they have absolutely no side effect as you usually expect from something looking like a property. Function like keyword is problematic too since you don’t expect a function to alter the control flow.

Postfix Macro syntax seem the least surprising since you can expect pretty anything from a macro.

2 Likes

We should also take for loops and patter matching into account when opting for an await syntax, as @withoutboats pointed out (https://boats.gitlab.io/blog/post/for-await-i/).

For loops:

// prefix await
for await result in stream
// mandatory delimiter
for await { result } in stream
// current macro
for await!(result) in stream
// postfix field
for result.await in stream
// postfix method
for result.await() in stream
// postfix macro with parentheses
for result.await!() in stream
// postfix macro without parentheses
for result.await! in stream
// postfix sigil #
for result# in stream
// postfix sigil @
for result@ in stream
// postfix sigil @ + await keyword
for result@await in stream
// postfix sigil ! + await keyword
for result!await in stream

Pattern matching:

match future {
  // prefix await
  await result => process(result),
  // mandatory delimiter
  await { result } => process(result),
  // current macro
  await!(result) => process(result),
  // postfix field
  result.await => process(result),
  // postfix method
  result.await() => process(result),
  // postfix macro with parentheses
  result.await!() => process(result),
  // postfix macro without parentheses
  result.await! => process(result),
  // postfix sigil #
  result# => process(result),
  // postfix sigil @
  // clashes with @ pattern binding
  // result@ => process(result),

  // postfix sigil @ + await keyword
  result@await => process(result),
  // postfix sigil ! + await keyword
  result!await => process(result),

In my view, prefix await, postfix sigil # and postfix sigil @/! + await keyword seem the most natural in the examples.

4 Likes

The lang team paper was a really interesting read, great way to summarize the discussion.

I find the field and method postfix syntax options really unattractive since they “hide” control flow in a syntax that usually has very different (simpler) semantics. On the other hand, I think space postfix could be workable (potentially as awaited rather than await to make it read a bit better?).

To me, prefix await could also work pretty well either with specialized await? or mandatory delimiters.

To zoom out a bit, I value familiarity more than (limited) non-orthogonality, and strongly sympathize with those who worry about the weirdness budget.

In that sense, I’m especially surprised that the “Order of Operations Solution” is discarded as too surprising while postfix field or method operators are still on the table. I feel like both of these options have a very strong element of surprise, potentially more so than the original order of operations solution.

I do feel it would have been very helpful to show large table with the potential options. I remember these helped quite a bit during the module system discussions to show the issues and help narrow down viable options.

10 Likes

Thanks for the writeup and for making progress on async/await!

I wanted to share some random ideas hopefully to stimulate thinking from a few angles that I haven’t seen represented.

When reading the introduction I wondered. Would it not be possible to adapt according to the situation?

await can only be applied to futures,
?     can only be applied to std::ops::Try

sure, sometimes these might collide, in which case there might be operator precedence or the user might have to put parenthesis, but that should be exceptional. The following seem unambiguous:

  • await result_of_future_to_result ??
  • await await future_to_result_of_future ?

I’m sorry if what I say does not hold for more complex situations.


I wonder how we want to take into account future requirements. It occurs to me that await with timeout is not really easy to express right now, but I imagine it would be a quite common need:

await.timeout( duration ) future   OR
future.await( timeout )            OR
future.await.timeout( duration )

I must say given await is an operation the postfix method syntax appealed to me more than the field syntax, especially if we might want to add the possibility of timeout at some point later. But, then a question arises:

Why is await not a method on the future trait?

Has the language team (or anyone else) carried out an experiment to convert existing code to async / await using one or more of the proposed syntaxes?

Not the Rust language team but someone did. For fairness, it should be noted that afaik he was a proponent of a postfix option before his experiment.

Github thread link to the specific comment

For more likely relevant comments from the long issue thread, I kept a bit of a journal along the way and (arbitrarily) selected a few comments that I thought contained new and/or insightful arguments. Await syntax summary · GitHub

3 Likes

I am not sure if this has been considered earlier or is too n00b but here it goes.

How about limiting the scope of things that can be done in an await statement? I propose that the only thing we can do with await is unwrap the future. Once that is done you get a regular Result and you can do regular rust things starting from next line. This side steps the issue of ergonomic way of unwraping the Result.

This makes the code slightly more verbose by forcing adding another line but keeps things familiar by keeping prefix notation and avoiding surprises of special method like calls.

You can use await on any future, since Future is a trait, any type could implement it. ? is controlled by the Try trait, which could be implemented by any type. There could be overlap where 1 type implements both, which is why we are having this discussion.

1 Like

I would like to offer a slightly different perspective from what I have seen here so far: which variants were chosen only for the sake of being “not like all the other features”?

I’ll start with the obvious contenders for that category:

result.await!
result#
result@
result@await
result!await

None of those are likely to ever be used anywhere else, they honestly look ridiculous, and if you show them to someone who has zero knowledge of why they were chosen, you’ll likely get a well-earned confused stare.

To make it even less compatible with the rest of the language, may I suggest using a Unicode symbol like U+2A20 Z NOTATION SCHEMA PIPING?

Next: “macro impostors”

await!(result)
result.await!()

As far as I’ve heard, a) await can never be an actual macro and b) postfix macros are not even a thing yet. The latter is kind of a minor point given that it’s never going to be a macro anyway, so why dress it up line one? I believe the standard macro-like syntax has only been accepted as a suggestion because of the “good old times” with try!() and should not be stabilised like this.

Now we are left with somewhat sensible proposals, so let’s look at the remaining postfix variants next:

result.await
result.await()

I’ll sort out the field access syntax first. Field accesses are at worst hidden behind a Deref chain, but NEVER activate any sophisticated mechanism themselves, so this syntax is a bad idea because it breaks that behaviour.

Then we have the method call syntax, and I’ve seen some people ask why await is not a method of Future in the first place. Regardless of the reason, it’s currently not, so this syntax should probably not be added until that changes. (Were it not for the “dishonesty”, the method call syntax would probably be my favourite.)

The last remaining candidates are:

await result
await { result }

At this point, it’s mostly a matter of personal taste. Me, I am a fan of only putting parentheses around things if absolutely necessary, so I would vote for the first one. It is less cluttered in all the common scenarios mentioned by other people like loops and generators, and await (result?) looks less visually disturbing than (await result)?, so I would also vote for await having a higher precedence than ?. Optimise for the simple case, because the complicated one will be cluttered anyway.

On the other hand, I can see why await { result } may be preferred. It is visually more in line with the rest of the language (think if statements and unsafe) and sidesteps the precedence battle entirely.

A perceived problem they both share is method chaining, e.g. (await (await x).y().z()).a().b(). While this is a valid concern, I don’t think facilitating a point-free programming style to this extent is worth accepting the inconsistencies in language design outlined by both others and myself in this thread.

I feel like a decision between the last two syntaxes cannot lead to a bad result, but all the other contenders have serious problems and while I probably have no authority whatsoever in this discussion, I would strongly advise against considering any of them for stabilisation.

5 Likes