A final proposal for await syntax

Right, any syntax would require you to respect the order of the await calls.

The point I raised in my post was not that + should be commutative. Instead I said out that swapping two lines that look like they only access a field (the .await syntax) now change the behavior of the program.

This is of course just a different way of saying that the .await syntax (in my opinion) is too subtle since it overloads an existing and very different syntax.

I call field access very different because it doesn't have side effects — but .await does.

The main argument against postfix macro await is that, await can not be implemented as a macro. While this is true under the current constraint, it’s not true if we implement await as our ordinary prefix keyword. (You may wonder ‘Wait what? Are you proposing a prefix syntax or a postfix syntax?’ Don’t worry. I’m going to explain that.)

Let’s recall why we needed to discuss the await syntax in the first place. The main reason is that, we want to chain await with method calls and more importantly, the ? operator. This problem can be generalized as, we want a mechanism such that we can chain some operations with some other operations. Surprisingly, this is not the first time we encounter this problem. And I’m not sure if the await syntax will be the last time we encounter this problem. The operator at the heart of await syntax discussion, the ? operator, is a feature we introduced to let us chain try! macro with method calls. While I personally enjoyed using ?, there’s no doubt that it’s one of the controversial feature. (From what I observed in different forums) Mainly because it adds a special rule to the rust language syntax to solve an ergonomic problem. And now we are introducing another special case, postfix keyword, to solve yet another ergonomic problem. I would agree with the choice if we had no other options. But I feel postfix macro would solve this problem perfectly.

So what postfix macro can bring to us?

  1. await can be implemented as a postfix macro, if we have an await prefix keyword.
the_bright_future_of_rust?.await!()?.is_here()?;
// expands to
(await the_bright_future_of_rust?)?.is_here()?;
  1. The ? operator can be replaced by try!.
the_result.try!().foo();
  1. Postfix keyword is possible
the_enum.match!{
  Foo(e) => e,
  Bar(e) => return Err(e),
};
// expands to
match (the_enum) {
  Foo(e) => e,
  Bar(e) => return Err(e),
}
  1. Or even
the_result.try_handle!(e => unimplemented!() // handle my error);
// expands to
match (the_result) {
    Ok(e) => e,
    Err(e) => unimplemented!() // handle my error
};

x.operator!(+, y)
 .operator!(*, z);
// exapands to
((x + y) * z)

You probably have noticed that, postfix macro is not just solving ‘chaining methods with result’ or chaining methods methods with await, but solving the root problem we have: ‘chaining some operations with some other oprations’. That’s why I find postfix macro is a real appealing solution.

In terms of implementation plan of await, we could take one of the following options

  1. Start with a prefix keyword and work towards postfix generic macro.
  2. Start with a postfix await special macro and work towards postfix generic macro, after we have postfix generic macro, convert the await macro to a normal macro with a keyword. With option 1, even if we don’t end up having generic postfix macro, we can still work towards posfix keyword as proposed in boat’s post. With option 2, even if we don’t end up having generic postfix macro, most users still wouldn’t have to know that await is not implementable as a macro.

TLDR: postfix macro solves the root problem of chaining operations. We should implement await using posfix macro.

link to original reddit post https://www.reddit.com/r/rust/comments/bmhmtw/what_postfix_macro_could_bring_to_rust_asyncawait/

6 Likes

You’ve overlooked that this also requires somehow making the keyword work as a macro. Not that it’s impossible, but it’s another thing that has to be done to make a postfix solution work, as all of await, try, match are keywords.

The corresponding postfix macro doesn’t need to have the same name though. They could be, await_for!, do_if, etc. or we could a macro, postfix!(keyword, …) to handle them all. The details definitely still need to be discussed. But the key point is still there, postfix macro solves the fundamental problem of chaining operations.

Sorry for the format, I’m on mobile.

Best regards, Weiwei Lin

I agree, though would nuance it a bit more to note that it's not just await, but await along with ?. Given that most futures are fallible -- to the extent that the original Future trait was only fallible -- the choice is not between await foo and foo.await but between some sort of await foo? and foo.await?. It's possible that, even if prefix await were better for screenreaders in C# with exceptions, .await? is easier than having them separate.

I think both sides can be argued reasonably here. One can also say that the goal in both cases is to make the code look as similar as possible to the infallible or synchronous code, only looking at the rethrow or interruption points later if for some reason you care (which you often don't), and thus being less in-your-face is an advantage, as it is for ?.

5 Likes

I strongly dislike postfix keyword syntax. Here’s why:

Readability: We can only say we “await something”

When we wait for a future, we say we await future, not future.await.

We say “future awaits” (future.await) when we mean a future is approaching, which somewhat makes sense in some other context but does not describe what we are doing.

What postfix await is trying to solve is but one niche use case

The only reason (that I’ve seen so far) to have a postfix syntax is method chaining and error handling:

let _ = future.await?.something_else();

But this is but one niche use case, is it really worth sacrificing readability and familiarity of classic prefix await syntax?

I strongly dislike postfix keywords (in general) as well

I mean, just look at this abomination:

codition.if { foo }.then { bar }

Alternate proposal 1: Postfix Symbol

I only dislike postfix keywords, but I am OK with postfix symbol, especially since I love using postfix ? syntax.

The above code snippet can be rewritten into this:

let _ = future@?.something_else();

Not only is this syntax way shorter than postfix await, it is also consistent with the rest of the language, especially postfix ? syntax.

Alternate proposal 2: Smart Pipeline

Dot syntax was never designed for method chaining, it just so happen that it is possible and JavaScript popularized it.

But pipeline operator was born for that purpose.

With smart pipeline, the above code snippet can be rewritten into this:

let _ = future |> await _ |> _? |> .something_else();

Or this:

let _ = future |> (await _)? |> .something_else();

Or this:

let _ = future |> await? |> .something_else();
3 Likes

We also say we "write to the file" and "lock the mutex", but in Rust that's file.write() and mutex.lock().

22 Likes

Further to @comex's point, you might say "Flatten an iterator" but we write iter.flatten().

If we have:

get_client_sme(wlan_svc, iface_id).await?
    .disconnect().await
    .map_err(|e| format_err!("error sending disconnect request: {}", e))

I would read this as:

Get the sme client with the wlan_svc and the iface_id, await it and re-throw, then disconnect and await, and map the error.

I don't think we are sacrificing readability. @scottmcm has made fine points about how .await and ? offers good readability and works well with left-to-right (data-flow order) reading flow.

We also disagree that it is niche. As seen in the code snippet oft cited in this thread, await is followed by . 20 times out of 43, which is 46.5% of the time.

Citation needed?

10 Likes

Good point

But that doesn't mean future.await is OK (to me). mutex.lock() cannot change the control flow.

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

Only if other languages also has .await.

The thing about .await is that it introduces an element of surprise (principle of least astonishment):

  • await is a keyword, but even a keyword is allowed to used as method name (JavaScript, and Rust too (remember the .then method?)), so a reader may read .await as a field access. And field access is expected to be cheap and zero-cost.
  • Absolutely nobody has ever use this syntax before, it only natural for them to expect await future.

My observation bias I guess.


So no one has anything to say about my alternate proposals?

Anyway, if you want to chain methods, you have to do it right! And .await is an ugly solution, it does not look right to me.

1 Like

Mutex in std::sync - Rust says:

This function might panic when called if the lock is already held by the current thread.

If .lock() panics inside function foo, then normal execution flow is put aside and flow returns to the callers transitively until .catch_unwind(...) or if not the thread panics. Rust's type system does not afford the user any means of proving that a function cannot panic and therefore .lock() can change the control flow.

Introducing |> is not a novel idea. Doing it just for the purpose of async/await seems to carry unwarranted complexity.

In Rust, chanining methods is done using ..

4 Likes

Good point. But .lock() is not an access field.

It may not be a good idea to implement this just for await, but this may be a good idea in general. Right now, we can only chain methods, not function calls and expressions.

Don't get me wrong, pushing for this idea right now is not what I am trying to do. What I'm trying to say is, even with prefix await future, it is still possible to make a chain.

I should have said "chain expressions" instead. Anyway, as I have said earlier, why only methods can be chained? Would it be cool if we can "chain" functions and expression into a pipeline too!


So, I am still against postfix keyword. Because it is unnatural and unorthodox. But I am not against postfix symbol, as ? is already a postfix symbol, adding another symbol (@) for async would solve what .await solves while still stay consistent with the rest of the language.

What do you think?

Not to put too fine a point on it, but all these proposals have already been stated by many other people and been discussed seven ways from Sunday, in several lengthy threads. Those threads are also still easy to find on the first page of Rust Internals and include, for example, (some of) @Centril's views.

17 Likes

The thing about innovations is that somebody has to be the first. So the argument that "nobody else has done it before" is the same as saying "nobody should do new things, ever".

At one point in history, prefix await was a new idea, that people didn't like because it was new. But people got used to it. The same thing happens with all new ideas.

Rust has quite a lot of new ideas (and new syntax) which haven't been done before. It does take into account what other languages do, but it doesn't just blindly copy them.

Unlike in JavaScript, you cannot use keywords as fields or methods in Rust. Please do not confuse these two languages, because they are very different.

9 Likes

Note that the criticism of .await is fundamentally different. When the await keyword was introduced other other languages, you did not see people complain that existing syntax in the language was overloaded to mean different things.

To make it crystal clear, Rust already has

  • expr.ident: this a field access
  • expr.ident(): it is a method call

The new proposed expr.await syntax clashes with the field access syntax. This is seen as a problem by people like me. Others think it's fine because they weight the ability to do expr.await? higher. This is basically what the discussion is about.

9 Likes

Then why don't we innovate completely: use the postfix sigil.

then is not a keyword in rust and if ... then ... is not possible in rust. Thy syntax is if a == b { do_this(); }. A list of keywords and reserved keywords can be found here.

4 Likes

Right, I am confused for a moment.

Personally, I think a sigil is a good idea. But the Rust team said this:

Other choices (like “ expression@await ” or “ expression#await ”) suffer from too much from the “line noise” problem and we strongly prefer to avoid introducing new meanings to punctuation characters for this purpose. For that reason, we discussed expression await as the primarily alternative to expression.await .

Essentially, they want to avoid adding new syntax as much as possible. They think that repurposing existing syntax is better than creating whole new syntax.

You can disagree with that if you like, but that is the Rust team's position.

There is a very good summary of the most common suggestions (and the Rust team's perspective on them): https://paper.dropbox.com/doc/Await-Syntax-Write-Up--Ac0thksDVSy3tvoeoAgO8BSnAg-t9NlOSeI4RQ8AINsaSSyJ

All of your suggestions have already been suggested before (many times!). You are not presenting anything new. This debate has been happening for months, with an incredibly large number of ideas suggested:

No matter what the Rust team decides, somebody is going to be unhappy. It may not be perfect, and not everybody is going to agree, but that's simply how life goes. It is literally impossible to please everyone, but a decision must be made.

And they made the decision in the best possible way, taking into account everybody's arguments, and thinking long and hard about this problem.

12 Likes