Await Syntax Discussion Summary

Thank you very much for putting this together!

I found the master and prefix-sugar versions more difficult to read for a reason I hadn’t really considered before: parens matching.

let response = (await wlan_svc.list_phys()).context("error getting response")?;

It takes some effort keeping track of the number of open and close parens, particularly when the initial “(await” is some distance away from the close parens. Also, sequential parens tend to blend together for me. It’s not as obvious at a glance which method call the await applies to.

On the other hand, prefix-mandatory was much better because of the use of a different delimiter and because of the space within the delimiter. I think that prefix-mandatory may also encourage breaking up async chains into sequential lines, which might be clearer.

I still think I like postfix-method (and postfix-macro) better because they are easier to write and because the action stays local (the () to me feels essential because they mark the point in code where something is happening) but prefix-mandatory is growing on me.

3 Likes

Speaking of my own code, I did have things like Result<StructContainingBoxedFuture, ...>. The Fuchsia utility in the linked repo also has a couple of places where something implementing Future is returned within a Result.

1 Like

Your experience is different from mine, then. I've wanted to chain after await decently frequently, and have been consistently say about its fixity every time.

I put a few examples in https://github.com/rust-lang/rust/issues/57640#issuecomment-457457727; to excerpt a few here:

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;
var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

This direction happened in the issue thread too, and I think it's the wrong thing on which to focus. Even if there were never a second await in a chain, that's largely irrelevant to me, because I'm interested far more in things like foo().await.collect() -- where postfix is still valuable -- than in things like foo().await.bar().await -- which I agree are rarer.

Of course, in complexity analysis log² x can also mean log log x (not (log x)²), so I'm skeptical of the value of that parallel.

9 Likes

Rust already has precedents using postfix where most languages use prefix: mathematical functions. Most languages use cos(x), abs(x), etc. while Rust uses x.cos(), etc.

8 Likes

Familiarity can be a double-edged sword, however, if the semantics are subtly different. I didn’t fully realize until I read some of the discussions (thanks for the links) that Rust’s await isn’t actually the kind of await you know from other languages. This will need to be explained very clearly in tutorials regardless of the chosen syntax.

Consider this well-known C# idiom for running two tasks in parallel, which will – please correct me if I misunderstand this – not work the same way in Rust:

// C# code
var task1 = StartTask1();
var task2 = StartTask2();
var (result1, result2) = (await task1, await task2);

In C#, tasks are running on their own, and the await merely registers something else to run afterwards – in this case, wait for both futures and collect their results. In Rust, and I admit that I don’t completely understand this yet, futures on their own don’t do anything unless they’re “driven” by an executor. Rust’s await construct has to perform some additional steps to accommodate this, so it’s not actually the same operator. It’s not merely an await.

And I could be wrong, but as illustrated by this example, I think the usage in even mildly nontrivial cases and the resulting idioms will be different from what you see in other languages.

6 Likes

I've posted an idea which generalizes @await approach and one proposed by @repax earlier:

4 Likes

One particular examples from https://github.com/inejge/await-syntax/blob/master/bin/wlan/wlantool/src/main.rs converted to use .await (replaced with .match for highlighting) (we can bikeshed formatting some other time):

opts::ClientCmd::Disconnect { iface_id } => {
    get_client_sme(wlan_svc, iface_id).match?
        .disconnect().match
        .map_err(|e| format_err!("error sending disconnect request: {}", e))
}

I find this quite readable and it seems quite ergonomic to write as well, especially when combined with “the power of the dot”. Meanwhile, await? didn’t seem nearly as useful as one might expect.

(I agree completely with @scottmcm that there need not be a second .await for chaining to be useful)

I counted 17 opportunities to chain, not 4.

That there were 4 times where .await could be chained in the same statement was quite surprising to me actually. I expected it to be far rarer than this, but 4 out of 40 in such a small bit of code is in my view significant.

14 Likes

I agree with what’s been said by others about the field-syntax future.await syntax, in that I don’t like how a keyword takes the place of what would normally be a field on a struct. Java does similar magic with the Foo.class operation, and the syntax there is equally strange. Kotlin (Another JVM language) does something similar but different in that it uses a different sigil for that sort of special operation, in its case it would be Foo::class. If future.await is the syntax that would be used, I would prefer something similar to happen to distinguish this as being distinctly not a field access. I’m not a fan of future!await, simply because ! doesn’t feel like a sigil meant to bridge a gap like that, so to speak. I’d prefer future->await if something of that nature were to be chosen.

Overall, however, I would prefer future#, despite it being terse it does stand out, and follows the pattern of try!() vs result?.

5 Likes

Why stop at macros? Use a special “postfix call” sigil and allow any callable thing.

We already have such a sigil, ., and it allows, e.g., when Trait is in scope, to either call a method via Trait::method(x) or x.method(). Adding a different sigil for doing that feels unwarranted.

We could have a #[lang_item] trait Awaitable { fn await...; } with blanket impls for all types implementing Future, so that you can write Awaitable::await(future), or future.await() but there are probably many other issues with this approach (const fn, unclear how it would work with streams, etc.).

If Rust had UFCS, then we could add a const fn await... intrinsic that can be used as await(future) and future.await(), but UFCS is a huge feature.

3 Likes

I interpret . more as “member access” than “postfix function”. It can be used to access member values and is only used to call functions if they’re methods, ie. member functions. It cannot be used to invoke, for example a free standing function, which I think is good. Member access should look different from invoking general functions.

However, there’s value in being able to chain non-methods like functions and macros like they were methods. I’m not advocating UFCS, because it goes a bit too far and pollutes the member namespace with non-member identifiers. Doing the same but with separate sigils for members and non-members would offer the best of both worlds without losing explicitness.

2 Likes

I think it’s a lot better for the postfix option to use the method .await() rather than a field. The field syntax .await is not orthogonal with the sense of accessing a field after .. It adds a yes but for async you have this special thing in a field which is not field itself.

On a broader view, await is a verb and not a noun which makes it fit better to be a function. If it was a field it should be a noun. And here, it has no meaning to be a field because a field, meaning “data”, has no real sense even on a more meta level. Which “data” are we accessing with .async? Does it mean it lives in the memory?

But having it as a function does have some meta meaning, you are .await()ing for the async function to return. You expect to have some business logic behind it, to process some stuff in the background to await what it says, and where the returned data will be from the async function and not the await itself.

4 Likes

That’s some super useful code samples! I think my favourite is prefix-mandatory but I don’t think I have ever chained await before so the main advantage of postfix syntax is not really worth the weirdness for me.

As Centril mentioned, there are 17 opportunities for chaining in that code.

See how it removes the temporaries and makes it easier to follow the control flow?

I guess it is different tastes? I find https://github.com/inejge/await-syntax/blob/prefix-mandatory/bin/wlan/wlantool/src/main.rs#L184-L188 clearer personally and I like the use of temporaries in the case of async code. I can see how someone would prefer the postfix/chaining approach though, I would probably still use temporaries in my own code instead of chaining if a postfix approach is chosen anyway.

7 Likes

The problem of the current await!() macro

I think it would help people understanding the situation better if the write-up elaborate more about this topic. I actually didn't see the problem of current async!() macro approach until I see @Centril's comment.

Which says that await cannot be implemented as a simple macro. Thus, even though we choose the current await!() syntax, it is not an actual macro but a compiler builtin that could not be written in the standard macro definition. Actually, Rust already have some of them: compile_error and assert. Now the question is: "do we want to put await on the same level with compile_error and assert?" My answer is no, because I strongly feel that async/await is a much more important feature than those and deserves a dedicated syntax.

#1 #2

Prefix vs Postfix

I once wrote a code which reads a row from a database and extract one of its field. If that was written in an asynchronous postfix await syntax, the code would be something like this:

let player_money = DB
    .player_table()
    .get(player_id).await?
    .money;

In prefix syntax:

let player = await? DB
    .player_table()
    .get(player_id);
let player_money = player.money;

Rust uses a method chaining a lot. Postfix macro is useful for a method chaining after await as discussed in the write-up, but I claim that it is also useful when a method chaining happens before await. I prefer postfix syntax over prefix syntax because it fits better to my mental model. When I wrote these examples, my thinking process flowed like this: "I have a database... now I have an access to the player table... and I want to get a row with this player ID... oh, that was an asynchronous method so I have to await it..." When I realize that a method that I was typing is asynchronous, I can add the postfix right after the method call. On the other hand, in prefix example, I should go back and forth to fill that missed await? .

That said, I think prefix variants with and without await? are both acceptable as long as it has the conventional operator priority.

Formatting postfix await

I believe whichever postfix variants we choose, we should keep that on the same line to the async method when formatting. This gives await keyword an exceptional feeling even if it resembles the existing language construct, which helps preventing misconception of the await keyword as a field, a method, or a macro.

Inconsistent candidates (field access, method call, and postfix macro)

I want to emphasize that how a syntax feels when I see the definition of it("field access syntax"), look at a short code(let val = fut.await?), and see examples from an existing codebase were all different to me. At first, I was very concerned about the inconsistency. However, after trying a few codes with them, I start feeling okay with the choice.

For some who hold this form consistency as important, it seems to be of paramount importance, affecting even Rust's credibility with end users as a seriously considered and well designed language.

That doesn't mean the inconsistency problem can be overlooked. The biggest problem of field access, method call, and postfix macro syntax is that they look like normal field access, method call, or macro. As noted in the write-up, using these candidates possibly harm the credibility and reputation of Rust. My first impression about them was the same with the above quote. This kind of negative first impression may have adverse effect on promoting Rust to other people.

Field access vs Method call

Right after reading the write-up, field access syntax felt like the worst of the suggested postfix syntax. Await is a very complex operation while field access is one of the cheapest operations in Rust. Their definition didn't feel align.

However, when I compare the candidates with actual Rust codes, I realized field access syntax looks better than method call syntax. For me, distinguishing field access style await from an actual field access was quicker than distinguishing method call style await from an acutal method call. One of the reason for this is the presence of ? after .await. Field access usually doesn't have following ? while method call and await operation often have following ?. Reading .await? gives me weird feeling and reminds me that it's an await operation, not a field access.

/* multi-line examples */
// field access syntax
let player_money = DB
    .player_table()
    .get(player_id).await?
    .money;

// method call
let player_money = DB
    .player_table()
    .get(player_id).await()?
    .money;

/* one-line examples */
// field access syntax
let response = reqwest::get("https://httpbin.org/ip").await?;

// method call
let response = reqwest::get("https://httpbin.org/ip").await()?;

#3 #4

Postfix macro

  • If there exists general postfix macro: similar problem with await!() macro
  • If there is no postfix macro except await: gives false impression to people that there exists a postfix macro feature

I think in either case, postfix macro is not a good choice.

Unusual other syntaxes

If Rust chooses unusual await syntax such as future!await or future@await, it can naturally bypass inconsistency problem. However, symbols such as ! or @ are too arbitrary for await operation. People would be curious whether there is any other operation in !op or @op form or not. Thus, I think the symbol used in this syntax should be as little obstrusive as possible.

I want to suggest ..await. There was a similar suggestion, but it didn't get much attention. ..await has similar advantage to field access syntax but doesn't have the inconsistency problem. Also, .. here gives an impression that await operation takes some time.

let player_money = DB
    .player_table()
    .get(player_id)..await?
    .money;

let response = reqwest::get("https://httpbin.org/ip")..await?;
10 Likes

Wouldn't that introduce an ambiguity with range syntax? Even if the ambiguity is resolved technically, I dislike the similarity of appearance.

13 Likes

What is the reasoning behind this? I don't think we have levels of importance by which we decide to have a dedicated syntax. I think there is a value behind simplicity and thus reusing things we know and have. There are many features in Rust that don't have a dedicated syntax because new syntax is costly. We don't "value" a feature with a new syntax. I find extension traits/methods super important for Rust, but we don't "honor" them with a special syntax to call them. So, no i don't think await is such important to be "honored" with a special syntax – in fact i think no feature is and i don't like the concept at all. We have dedicated syntax for other reasons. Even if we would do that i don't think await is that important either. Yes its the "must have feature" and the most discussed currently. But that will fade quite fast.

I also don't think there is any problem with postfix macro syntax and that await can never be a "real" macro and "just" a compiler thing. No one ever complained about the other macros that are (at least i have never heard complaints until now) I don't see this as a very strong argument. If there is a possible future with postfix macros we should not waste our syntax budget on a strange single usable syntax. If we don't get postfix macros this syntax is just as weird as any other postfix syntax except with the possibility to become a universal thing. "I am {} years old".format!(42); does not sound to crazy from my point of view.

8 Likes

AFAIK writing ..await is technically possible because await is a keyword. I thought the similarity to the range syntax was minor so I didn’t mention it, but if it causes a problem we can use more dots maybe? :wink:

1 Like

It's worse than this. While assert is a built-in macro, it is legal to write:

macro_rules! assert { () => {} }

However, we want await to remain a keyword to have future design freedom. To achieve that, .await!() would need to be a special hack in libsyntax just as much as .await would be. However, .await is already an existing syntactic form in the parser so the simpler change to the grammar is .await, not .await!().

2 Likes