A final proposal for await syntax

Normally I do read code in the way you suggested, but when I notice that I’m in an async context my mental model (from doing C# / JS / F#) is to first note an await in an expression, then follow the code (as expressions/statements start with await). With postfix syntax I tend to scan around for the awaits, then start reading the expressions.

I don’t know if this is just purely habit or because I like to know where I can expect executions to be deferred before getting into the core logic

4 Likes

I forgot to answer the second part of your question. I’m going through the code differently than I do with the ? operator (where I still naturally read left to right), which suggests to me prior experience with async in other languages has created certain expectations for me

3 Likes

This could be relevant regarding if Perl 6 has postfix unary named operators?

If the answer to that is: ¡¿What are you crazy? We just use a function for that, of course! ...then that is highly relevant to await syntax.

If they have postfix unary named operators, any sigil they chose could be relevant as prior art.

That they exceeded the ASCII (mental) barrier, is about as relevant to await syntax as it would be for Rust accepted RFC 2457—only tangentially.

If you want to fairly critique my ¡await proposal, then you are too fixated on "non-ASCII", and not considering the nuance in selection of typography appropriate, discernible, and aesthetically pleasing for this use case.

Design is horrifyingly analog to computer scientists. Its about balance and position in context. Here the analog pendulum has swung from the very conspicuous (some argue typing intensive) prefix keyword or function all the way over to something that relies on highlighting to distinguish from the most fundamental field access. But committee design process favors binary logic and decision. The committee will accept a series of half-truths to explain away all problems with .await and to dismiss all alternatives.

1 Like

Speaking of precedence, Swift allows this (postfix unary operator, non-ASCII), even as a user extension:

postfix operator ☭
postfix func ☭(l: String) -> String {
    return "\(l), Comrade"
}

print("Good Morning"☭) // Good Morning, Comrade

I sort of wish the author had avoided the communism meme. More seriously, I do think Rust should continue to play a role as a wiser and more conservative older uncle to Swift. But again, its a matter of degree and balancing goals.

source: https://hackernoon.com/creating-custom-operators-in-swift-2c8bababbaf6#d4fa

Moderator note: This particular thread is veering into non-constructive territory. A discussion on await syntax needn’t devolve into pronouncements and debate over project governance. Moreover, declarations that the lang team are “accepting a series of half-truths” are not considered by us to be engaging in a conversation in good faith. If this continues, then we’re just going to lock the thread.

As always, if you have questions or concerns about moderation, do not comment here, but instead, email the moderators: rust-mods@rust-lang.org

13 Likes

I’ve just noticed !await postfix proposal somewhere in this thread. While I do not like postfix keywords (such as .await, .match, .if) at all, postfix macros is something I can get behind. We also no longer need to have await as a keyword as we already have await! macro.

await!(future).foo.bar

can be rewritten into

future!await.foo.bar

and

try!(result).foo.bar

can be rewritten in to

result!try.foo.bar

This makes it so that this code:

future
  .await?
  .parse()?
  .link()
  .fetch()
  .await?

would now look like this instead:

future!await!try
  .parse()!try
  .link()
  .fetch()!await!try

This proposal is, in my opinion, pretty elegant.

As explained in the proposal document, Niko's summary comment, and elsewhere, this is not what "postfix macro" means. This is what "postfix macro" would look like:

future.await!()

In other words, it's normal macro syntax (await!()) following the dot (.) operator.

Also, please note that there have been several explanations of why await must be a true keyword and cannot be implemented as a macro (postfix or otherwise).

8 Likes

In the last posts by @KSXGitHub and @BatmanAoD there are three valid alternatives mentioned, and it would be unproductive to not consider them individually.

await!(future)

As currently implemented as a nightly-only, unstable variant. The major point against is that it can not be implemented for this same purpose in user code.

My opinion: Big deal! Its less of a special case than .await. Its visible when used. Less will be lost with this option among these three alternatives and .await.

future!await

(as last mentioned by @KSXGitHub, @gbutler)

This is a member of the “alternative sigil with keyword/identifier” collection of alternatives, along with future@wait, future#await, future¡await, etc.

My opinion: This sigil is the least appealing among this collection. To me it reads like future! is the macro, and somehow parenthesis have become optional for its await argument. I think if I saw 5+ uses of this form I might have a seizure. It has a strobe like effect on my brain.

future.await!()

Made the @nikomatsakistradeoffs of postfix dot notations short list and last mentioned by @BatmanAoD. Previously proposed as RFC 2442, see comment.

My opinion: Also better than .await, however I don’t like the precedent without the RFC accepted, and of these three options, would prefer the “regular” macro (first in above list): await!(future).

1 Like

I think we should split this proposal into 2 parts:

  • Normal await syntax
  • Chainable await syntax

“Normal await syntax” is to find a good enough await syntax. It can either be await future or await!(future). In my opinion, await future is good enough. Why and how I already explained.

“Chainable (postfix) await syntax” is to find a way to chain await into a series of field access and method calls. It can either be future.await!(), future@await, future@ or what have you. This should be discussed after prefix await is used for a while.

1 Like

I know this thread is long, but please try to determine whether suggestions have already been made before you post them as new ideas. (I realize that having a canonical summary post would help with this, but unfortunately the closest we have is Niko’s summary, which, by design, is not comprehensive; I’m sorry about that.)

Having multiple simultaneous proposals would imply that we would be open to stabilizing multiple syntaxes. This has been discussed above, and while there was some support for the idea, I believe the majority of feedback was negative.

10 Likes

Should we support both prefix and postfix syntax, just like function call and method invoke?

prefix & function call

await fut

await? fut same as await(fut)?

Iterator::iter(list)

postfix & method invoke

fut.await

fut.await?

list.iter()

I think we should support prefix await fut as primary syntax and make it possible to chain await. I am sure this has been considered before (such as postfix keyword, which I don’t like, as it is just a useless variant).

The reason for postfix dot-await is to make it possible to put await (and only await) into a dot-chain of field access and method calls (and only field access and method calls) without making too much noise. Now, if you look back at my previous sentence right before this and remove the italic “and only” part in braces, it would sound like a good reason. But I put them there to emphasize its niche use-case, it is a poor excuse.

I’d like to offer a more thorough set of proposals:

  1. Just use the “good enough” prefix await as the primary syntax. This is similar to function calls, where a function name always precedes input.
  2. We attempt to solve the chaining problem of everything prefix (such as await future, function calls, macros) by introducing some kind of pipeline.
My arguments for introducing pipeline

The second proposal would take more time to implement but I think it is inevitable anyway (sooner or later, the needs of chaining multiple normal functions into a pipeline would arise, and a proposal for the particular problem would be made).

Argument has been made that the Rust way is to use dots to chain things. Although I agree this was and is the case right now, I also think that this way is very limited and we should change.

We made it possible to chain not only field access and method call, but also error handling (via ?), and await too. So why not go a step further, allow us to chain everything: function calls, expressions, macros, prefix await, etc.

1 Like

Another reason that I think postfix dot-await is a poor approach is inconsistency.

Dot-chain was for dot operations (field access and method call) only. This is true in virtually every language (that has field access and method calls) that I had encountered.

Rust had made a decision (more like an exception) to enable integrating error handling directly into method chaining (result?), and Rust is going to make another exception (more of an exception than even error handling, since await would be the only keyword to be after a dot) to enable await into method chaining too.

Making too many exceptions would, in a long term, turn Rust into an inconsistent/incoherent mess of a language.

2 Likes

This begs the question, can't await() be a member function of a special (lang) trait? Kind of like Clone, Drop or the others. Clone in particular is a fitting precedent, since it's implementation is both complier-generated (in some cases) and it has a member function too.

(I'm sorry if this has already been considered and I'm making noise, I tried to skim the thread but I could've easily overlooked something.)

It had already been explained: Function calls and method calls cannot change the control flow and are expected to not change the control flow, unlike await. Ironically, this is one of the arguments against method-like .await() in favor of field-access-like .await.

That is a valid objection but it doesn't seem to me to be such a showstopper, since

  • A special trait can reasonably be expected to do special things
  • Functions can already affect control flow via panicking
3 Likes

This has already been discussed before here

Specifically the trait idea comes up here

Here is what I think is the best reason not to do it

8 Likes

Allright, thanks for the links, I won’t press the issue further then. At the end of the day it’s just syntax and I think the current state is ok or at least not worse than alternatives…

1 Like

I can mostly accept that there is a “surface representation” for the await operation, which users will work with, and an internal representation (state machine generator yielding ongoing state). With this mental model though, arguments of the form “it can’t be a function/method/macro” fall flat for me, because it can’t be a field access either, and .await makes no attempt to differentiate itself from field access. Its all surface representation. The original code AST gets transformed, so any of these syntaxes are viable, and it becomes solely a question of which works best for learning and long term use.

Changing the sigil fixes it. Even just changing the keyword case to future.AWAIT would fix the problem for me, both in terms of seeing the control flow operation and it not being confused with field access. Was that ever considered?

My workaround, if nothing improves, will be “unusual” whitespace (which will likely annoy the rustfmt enforcement camp, oh well, see my samples above) and/or surrounding comments:

future /*magic «*/.await/*» control flow*/
    .context("so refreshing")?;
3 Likes

This is an interesting idea. Postfix keywords must be ALLCAPS. That doesn't sound horrible and would make it stand out better. Also, IDE's could easily be trained to automatically change .keyword to .KEYWORD for Rust. Then, all keywords would just need to be reserved in both their lower-case and upper-case versions (which, unfortunately, would be a breaking change, but, one that might not have any real impact).

2 Likes