Await Syntax Discussion Summary

As Centril notes, we don’t have prefix operators.

return and break are “prefix operator expressions”, sort of, but they also evaluate to ! and 99% of the time are used as if they were a statement. (The 1% is as match arms.)

loop, for, while, and that kind of construction are not prefix operators. They don’t take a single expression/value as an argument; loop takes a block, while an expression and a block, for a pattern, an expression, and a block.

A prefix await would be just as novel syntax as a postfix await in effect.

(Side note: I’m strongly against “optional delimiters” if they change precedence. await (⟨Expr⟩)? should be await ⟨Expr::Paren ⟨Expr⟩⟩? and not ⟨Expr::Await ⟨Expr⟩⟩?.)

3 Likes

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? This is probably a lot of work to do, but might be worth it in this specific case in order to find out how it actually feels to use each of these syntaxes. e.g., It might turn out that await? foo() actually makes a lot of sense and that the value of chaining is less than one thought.

23 Likes

Small-ish experiments has been conduced with real code on the Resolve `await` syntax · Issue #57640 · rust-lang/rust · GitHub issue.

All unary keyword operators in Rust are prefix, and with the exception of the infix as operator, all constructions (at the item, type, and expression level) involving keywords involve a keyword as the initial token of the sequence.

I think in might be missing (for x in y) unless it does not count.

For that to happen, though, there is general agreement that we should first resolve the syntax (as opposed to stabilizing the await! macro)

The document does not argue why should we block the whole async history on this: we can always add a syntax later, and deprecate the await! macro after that happens. For try! that worked ok. Why wouldn't that work here?


EDIT: AFAICT stabilizing the await! macro has much less problems than any of the proposed solutions. Sure, its not "as great as it could be", but it would be uncontroversial, it would work, and it would let us do better in the future.

4 Likes

in doesn't count: the syntax is for ⟨Pat⟩ in ⟨Expr⟩ ⟨Block⟩ and not for ⟨Expr::In⟩ ⟨Block⟩; in means nothing without the for.

3 Likes

Thanks, but that link isn’t particularly useful unless you’re willing to sift through the ~500 comments in that issue. Were there any interesting conclusions drawn from the experiments that can be summarized?

3 Likes

This knowingly adds technical debt, eventual churn, and obsoletes existing documentation. In particular, I don't expect things to change in 2 years time. The same arguments will be had again and again.

For try! it was socially costly and it still made things obsolete. One notable difference here is that try! is a user space macro whereas await! cannot be. It must be implemented in the compiler which makes the technical debt that much worse.

It is not uncontroversial. await!(expr) is a prefix syntax and moreover encourages parenthesis buildup. This same form of argument can be made for expr.await; the await!(expr) form just happens to be what exists today.

I don't think many shared conclusions were drawn but it did affirm my view that postfix is readable and hard to miss, that chaining is useful, and that prefix doesn't compose well and introduces temporaries. (I have read all of the ~500 comments but I don't have it in memory)

7 Likes

could someone please reupload the paper linked in the original post some place that does not require an account to view it? as @dlukes mentioned, the link goes to a dropbox login page for me. thanks :slight_smile:

2 Likes

Link from reddit: https://pastebin.com/VrdH0UDN

2 Likes

If the await semantics that is planned to be stabilized couldn't be implemented a normal macro, I think now is the correct time to choose a dedicated syntax for it rather than keeping the current await!() syntax and postponing the keyword selection.

2 Likes

I think it can be “fixed” by allowing procedural macros to query (or accept) information about context surrounding macro. This way await macro (prefix or postfix variant) will be able to return error early if it’s used outside of async context. (though reusing closure syntax for generators also does not help here…)

One can even think about using custom contexts for DSL extensions, i.e. you will wrap your code in some macro (à la #[async]) which will set the context, and then inside this context you will be able to use DSL-specific macros (await!(..)).

  1. I wholeheartedly agree that a postfix syntax is the better option that a prefix syntax.
  2. I am not convinced that a sigil is too weird and is obviously a worse option than a keyword.

If asynchronicity will become pervasive in Rust code, then the syntax must be as smooth as possible. The language has already adopted a better syntax for try!() and a pattern for unboxing is in the works. If every other line will have two .await!() in it, the code will become noisy.

14 Likes

The orthogonality argument is weak, in my opinion. We’re mixing field access syntax with control flow. That’s not orthogonal at all.

2 Likes

@Centril

This knowingly adds technical debt, eventual churn, and obsoletes existing documentation. In particular, I don’t expect things to change in 2 years time. The same arguments will be had again and again. For try! it was socially costly and it still made things obsolete.

The argument that new language features can make code, documentation, etc. obsolete is true for all language feature (e.g. a foo.await feature makes all code currently using await!(expr) obsolete). Therefore, I don't personally give it much weight.

One notable difference here is that try! is a user space macro whereas await! cannot be. It must be implemented in the compiler which makes the technical debt that much worse.

The maintenance of the await! macro after new stable await syntax is introduced would be zero - that's what stable means: we update the macro once to use the new syntax, and then it never breaks.

the await!(expr) form just happens to be what exists today.

And stabilizing that would remove the churn caused by an unstable async/await ecosystem, which is a much bigger problem than the potential of having to deprecate a trivially-to-replace macro in the future if we are all ever able to agree on better syntax. Making the async/await ecosystem suffer this churn has been feeling unnecessary for months already.

EDIT: so I think the text should properly motivate why it exists: why should any stable solution to the problem be blocked on us agreeing on syntax when a macro can be provided today? The text starts from the position that this is the case and there are no alternatives to that, but AFAICT this is not the case. One alternative is stabilizing the status-quo, as long as the problems can be resolved without breaking backward compatibility incrementally later.

11 Likes

What exactly are the arguments against await!? The only one I've seen is that of @Centril which is that there's already an await reserved keyword, thus we should use it.

2 Likes

Before I read the write up, I’m kinda in favor of prefix… but after reading it I started thinking postfix is probably better. Also I think it would be cool to have await be separated by whitespace, so that if you don’t have much stuff, you can do:

let foo = get_foo() await?;

and in case you want to chain multiple await, you can

let foo = get_bar() await?
    .get_foo() await?;

which seems to be quite nice.

7 Likes

await! is notably unstable which in my book makes all the difference.

As noted by @Nemo157 on Discord, if you actually stop baking await!(...) into the language itself, then you need to either 1) start by using r#await!(...) as the concrete syntax, 2) accept edition differences. Moreover, it adds debt in the reference, in guides, in tooling, in other compilers, and so on.

This does not justify why await! is the temporary syntax we choose assuming we are OK with a temporary one (I'm not). One could say that the churn of existing nightly code should be considered, but I won't do that because the minute I do, then nightly starts becoming meaningless.

There are implementation blockers for stabilization irrespective of the syntax issue and has existed for months. So I think this doesn't hold.

The same argument against try! and prefix in general.

1 Like

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