Await Syntax Discussion Summary

Quite a while ago, Niko promised that we would write a summary of the discussion in the language team and the community about the final syntax for the await operator. Our apologies for the long wait. A write up of the status of the discussion is linked below. Before that, though, let me also give an update on where the discussion stands now and where we will go from here.

Brief summary of where async-await stands right now

First, we hope to stabilize async-await in the 1.37 release , which branches on July 4th, 2019. Since we do not want to stabilize the await! macro, we have to resolve the syntax question before then. Note that this stabilization doesn’t represent the end of the road — more the beginning. There remains feature work to be done (e.g., async fn in traits) and also impl work (continued optimization, bug-fixing, and the like). Still, stabilizing async/await will be a major milestone!

As far as the syntax goes, the plan for resolution is as follows:

  • To start, we are publishing a write-up of the syntax debate thus far – please take a look.
  • We want to be forward compatible with obvious future extensions to the syntax: processing streams with for loops in particular (like JavaScript’s for await loop). That’s why I’ve been working on a series of posts about this issue (first post here and more coming in the future).
  • At the upcoming lang-team meeting on May 2, we plan to discuss the interaction with for loops and also to establish a plan for reaching a final decision on the syntax in time to stabilize async/await in 1.37. We’ll post an update after the meeting to this internals thread.

The writeup

The writeup is a dropbox paper document, available here. As you’ll see, it is fairly long and lays out a lot of the arguments back-and-forth. We would appreciate feedback on it; this thread is the appropriate place to leave feedback.

As I said before, we plan to reach a final decision in the near future. We also feel the discussion has largely reached a stable state: expect the next few weeks to be the “final comment period” for this syntax discussion. After the meeting we’ll hopefully have a more detailed timeline to share for how this decision will be made.

Async/await syntax is probably the most hotly anticipated feature Rust has gained since 1.0, and the syntax for await in particular has been one of the decisions on which we have received the most feedback. Thank you to everyone who has participated in these discussions over the last few months! This is a choice on which many people have strongly divergent feelings; we want to assure everyone that your feedback is being heard and the final decision will be reached after much thoughtful and careful deliberation.

65 Likes

This is a great summary, seems a lot of effort has been put into making the case for every design. (I appreciate the table covering ? and ., the . aspect of the discussion is new to me.)

A small addition might be in order here- the same could be said of prefix await with delimiters.

10 Likes

Why not just stabilize the await! macro? Let it go through the same process that try! did: let it gain adoption, look at how it ends up actually being used in practice, and then use the new information to design something more ergonomic. The writeup mentions that there is agreement on not taking this path, but I don’t see elaboration on why. Maybe I’ve just missed this in past discussions.

This in my mind would solve the issues of weirdness (it’s just another macro), orthogonality (it’s just another macro) and precedence (macros require some kind of brace/paren).

33 Likes

Before reading this great write up, I was in favour of await?. I’m now swayed to postfix by:

  • Orthogonality
  • Reconsidering “it isn’t immediately obvious from the syntax await? foo() whether the ? occurs before or after the await.”
  • Postfix is less weird than await? (foo()?)
  • Reading code with prefix await involves the same mental juggling the try! macro imposed and reading the postfix operations in sequence imposes less cognitive load.
  • Since await is only used in async contexts, highlighting the await operation by having the keyword at the start of the line is not that important. When reading/writing an async fn, I already need to remember I’m in an asynchronous world.
  • As postfix ? showed, once we get used to it, with syntax highlighting it’s easy to notice things in the middle/end of the line.

Reduced cognitive load trumps weirdness. We become used to things and they’re no longer weird, but increased cognitive load makes reading/writing code more difficult forever.

32 Likes

My main concern is that the await!() macro as it currently exists seems to have already been discounted. If you already know macros, which if you’ve used println!(), you likely do, then you can determine the usage of await!(). A big concern I have about field-like syntax is that it doesn’t suggest that it’s doing anything. Typically, field access requires nothing more than a pointer dereference (which in Rust translates to a Deref::deref() call). I would probably be more on board with postfix syntax if it was a sigil, but there really isn’t any that would make sense.

If it was field or method-like syntax, what would happen with struct FakeAwait{ await: Something }, or impl AwaitMethod { fn await(&self){} }

10 Likes

Because await is a keyword, you can’t have a real field/method named await, only r#await if you really want to call it await.

4 Likes

Good point.

I understand why the sigil alone was ruled out, but considering another sigil along with the await keyword seems like it resolves some of the issues around confusion with attributes/functions.

future@await future?@await future@await?

which seems like it would combine better in the for-loop case as well.

In syntax highlighting styles if it’s just the await keyword then await needs to be colored boldly or mildly. If bold (eg red) then it dominates attention over the content part of the code. If a mild color then it risks being lost in a chain of finding and methods and the prefix argument stands that it’s important to notice.

If you have a sigil-await combo then the sigil can be bold without dominating the line’s content.

6 Likes

the Orthogonality argument convinced me

im now on the postfix camp

7 Likes

I concur with @elahn. I was previously in the camp that the prefix based await? was better because “it’s what all the other languages do”. I am now under the impression that the orthogonality of the postfix syntax has many benefits.

The ease of chaining without nesting many parenthesis is a welcome change from the current await macro and the prefix await. The readability like a method that supports Try! is clear to me as a reader of the code as well. It’s for those reasons I personally favor the postfix method syntax.

5 Likes

The orthogonality argument feels really unconvincing since it requires breaking what people have already learned about field access syntax.

Having magical implicit fields feels strictly worse than having to type extra parens.

23 Likes

We also have consensus among the lang team that some degree of familiarity with the syntax of this feature in other languages is important. In particular, we note that this feature has come to be identified with the addition of two new language forms: an async operator and an await operator, to the point that the feature itself is referred to simply as "async/await.” We have consensus that this argues strongly against using a sigil syntax, or any selection which does not contain the string "await” as a part of its syntax. For that reason, we have excluded sigil based syntaxes like @from further consideration. If we adopt a postfix syntax, it will include the await keyword in some way.

I don't agree with this summary conclusion. I personally read this as verbal gymnastics being used to avoid coming right out and saying "I feel like this sigil would be too implicit", since we all know that implicit/explicit is not great for discussion. I think someone made a comment around the time of this blog post along the lines of "people clamor for new syntax to be as noisy/explicit as possible because they're unfamiliar with it, but then they push for that syntax to be made shorter and more implicit as the feature ages". A great example of this is, of course, the try!(expr) -> expr? syntactic sugaring that happened.

I actually think that try! is a great analogy for await! on the whole. try! branches the code flow under the hood, just as await! does.

I completely agree with some comments that postfix is likely the way forward, since it avoids many ambiguities. I think that @ is conditioned into many people that it should be followed by something else, in large part thanks to email addresses, so I don't think @ would be the way to go. But, consider:

async fn example() -> Result<String, Error> {
    let mut body = get("rust-lang.org")#?.body;
    body += some_footer();
    body
}

A simple example to start with, but let's focus on the question mark for a second. Absolutely no one in this discussion is confused about the meaning of that: it will take a Result and early return if an error is returned from get()#.

If a new Rustacean comes from, say, C#, they're going to see the question mark and think it's a null-safe coalescing operator, rather than something which is going to alter the control flow of the function.

I think it's safe to say that this will not be a substantial obstacle to learning Rust. A few moments of confusion, easily cleared up by a quick google search. The try syntax is clearly highlighted in most text editors, so it doesn't go unnoticed. All of the clamoring for explicitness and noisiness just seems like much ado about nothing at this point, does it not?

I propose that # would be a fine, standalone suffix sigil for this application. Even @ would be more preferred by me than .await or .await(). Actually, let's take a quick detour to address those syntaxes. While they avoid the ambiguities of prefix notation, they introduce new ambiguities: many people will surely be confused into wondering why these structs have an invisible field or method named "await" upon first encountering this syntax. I personally believe this confusion will be equal or greater than the confusion they would experience upon encountering a new sigil.

If we want to get symbolic, the # represents the crossroads of several concurrent tasks, or we could look at how this symbol represents "equal and parallel to" in mathematics, and concurrent operations feel parallel. But, the reality is that symbolism probably isn't important here. People would get used to it, and it stands out instantly from normal Rust syntax, satisfying the crowd that wants easy visibility into control flow operators like ?. It will be trivial for editors to syntax highlight this operator with confidence.

In a few years, I feel reasonably confident that the story of try! will repeat itself if we take the verbose path now. People will tire of typing .await or .await(), and they'll pine for a less noisy way to handle the chained futures.

I would know: I've written some not insubstantial production code using tokio and futures! The example above is idyllic. A single future visible in this function. The reality would look a lot more like this:

async fn example(db: DB, hash: Sha256Hash) -> Result<String, Error> {
    let body = add_footer(get("rust-lang.org")#?.body#?)#;
    db.store_body_if_hash_eq(&body, hash)?#?; // hash mismatch returns error immediately, database store might return error after the future is resolved
    body
}

In Summary:

Using .await will make lines longer and harder to read, involve more typing, and all for the sake of "familiarity"... but is it really so familiar to see virtual fields or methods? When I see a field .await, I definitely don't think "this is going to change the control flow". When I see a method .await(), I think someone is at best using futures "wrong" by invoking them in a synchronous manner.

When I see #, I realize that I simply need to learn about this syntax. I don't apply any false assumptions to the behavior of the code, assumptions which will delay true understanding since I won't feel the need to research this new control flow immediately.

Sorry if this got a little long, I just had a lot to say.

37 Likes

Great write up! I really appreciate having the major points covered and this does a good job explaining the trade offs.

I also am falling in the postfix camp after reading this bu I’m a -1 on field access

As an example:

foo()?.bar().baz().await.alice()
  • Too hidden when looking at code and that we should have a dedicated syntax for post-fix operations.
    • It is very easy to miss the await. Some might say “that will be solved by syntax highlighting” but (1) not all scenarios will people have syntax highlighting and (2) personally, I prefer my languages to be helped by the editor but not require editor features.
  • Most other keywords have a strong delimiter (space if not some other punctuation) before and after
  • Field access has the feel of a passive operation and not something so active
  • EDIT: All this makes it too surprising for someone approaching Rust code. It makes the weirdness and surprise budget higher than it needs to be for postfix.

The question then is how to specially mark await. To start:

// schuyler1d 's proposal
foo()?.bar().baz()@await.alice()
// My original thought; think of it "piping" the data through the keyword
foo()?.bar().baz()->await.alice()

I also feel like there should be and end delimiter but I’m a little more unsure on how to handle this. Some random thoughts to seed conversation:

// Special prefix without suffix stil feels passive (we're used to `()` to say "something happened"
foo()?.bar().baz()->await.alice()
// Function feels too bland still
foo()?.bar().baz()->await().alice()
// Variations of macros feel a little better
foo()?.bar().baz()->await!().alice()
// This variation feels like it strikes the right balance if it wasn't for taking "the macro sigil" and removing the call part
foo()?.bar().baz()->await!.alice()
2 Likes

This reminds me of a point that I didn't see covered in the summary: aren't we likely to see other keywords that suffer the same problem, like a yield for generators / iterators / coroutines? A sigil just for await doesn't help us with these other cases.

7 Likes

I haven’t been following yield and generators as closely, but I think yield will likely always be better as a keyword, because I’m fairly certain it won’t be nested multiple times within a single expression. yield will always be the least binding keyword, right? Whatever is to the right of it, all of that should always be evaluated before yielding.

If there are cases for nesting yields within a single expression, similar to await, then I’m happy to change sides of that fence, but the big reason for wanting postfix-await is to help with nested expressions.

11 Likes

I think having both a start and end delimiter is overkill, just one should so. I agree that future.await/future.await() is too hidden and will probably be confusing to beginners. The future@await syntax looks good, it keeps the brevity of .await, but makes it clear that there is something special going on here.

6 Likes

Like you, I've not been following it closely. If yield is just for iteration, then that can work. The problem is when you have a keyword for coroutines. At that point, the keyword can return values. This is the approach at least python takes: yield accepts a value, throws exceptions, and returns values.

If we’re going to use the literal word await in postfix, I really like the -> syntax, but I still favor the simple postfix sigil that seems the logical conclusion of all of this, several years down the road.

4 Likes

I still think you’re only going to end up with one yield per expression in the majority of situations, but I can see where you’re coming from.

let x = yield blah

or

for x in yield blah

is all fine with me, but if you commonly need to do something like

let x = (yield blah(x - yield foo)) + 3

then that starts to get hard to read

While there won’t likely be many yields, there is still the error handling binding problem

let foo = yield bar()?;
// Which should that be?
let foo = (yield bar())?;
let foo = yield (bar()?);

On an unrelated note. for already has the error handling problem. The number of times that I need to do

for foo in bar.iter() {
   let foo = foo?;
   // ...
}
1 Like