Await Syntax Discussion Summary

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