Await Syntax Discussion Summary

I’m not sure that the await!(fut) macro syntax is such a bad option, I do wonder in practice right now how bad code written using this syntax is (is parenthesis build-up really such a problem and is it really too verbose)? Do we have any statistics or comparison outside of hypothetical examples which we could use as a base for a decision? Personally I wouldn’t opt to just stabilize any syntax without trying it out for a longer period.

As for the other options available (if the await!(fut) syntax really is off the table), I would personally pick the fut.await!() syntax (with the fut.await! a good second choice). It looks somewhat like a method call, but the macro-like syntax sets it apart to make sure it isn’t confused with a normal method call, although users may interpret it as such in an async context without too much trouble. Coincidentally it also would suit a possible future postfix macro extension really well. Personally I’m strongly against the fut.await syntax. It looks the same as field access, whereas the semantics are completely different, this would give it a high surprise factor for me. The other prefix options (other than the macro syntax) feel bad because of the orthogonality argument, most of those options don’t work well with other Rust features.

I would aim to prevent something like fut@? or fut#? (as a syntax for awaiting and then checking the result), as it would make Rust code a little too dense for reading in my opinion (which should be just as important a factor as how easy it is to write it).

1 Like

Some extra points I want to make:

  • I don’t find the syntax highlighting / IDE support if favour of postfix await compelling. The preferred editor in the Rust community seems to be VS Code, and its syntax highlighting support is pretty bad (via).
  • await is pretty similar to yield, since they’re both control flow operators that work in a similar way (actually, I used yield return to emulate await before C# had it). If we go for postfix await, how would that extend to yield in the future? E.g., do we want the following?
for i in 0..100 {
    i.yield;
}
  • I don’t think the builder pattern and code that crams a lot of stuff into a single expression are always desirable. It might make sense for builders, but it comes with disadvantages like worse support for debugging.
3 Likes

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.