Await Syntax Discussion Summary

Absolutely. Since several compiler built-ins already have macro syntax (heck, even format!() which is arguably quite important), there's not much reason to come up with new syntax just for its own sake. In fact await!() is similar to already-existing built-ins and other macros in that it makes sense in expression position, it affects control flow, it expands to more, lower-level code in-place (even if that code in question implements a generator), etc.

Of course, how it works is unique, but what it does is not at all much more (or less) abstract than the aforementioned macro-like built-ins.

4 Likes

Except: cargo expanding code with format! and assert!, while they may be built-in macros, still expands the macro. Semantically, they expand to “plain” Rust code and could be reimplemented outside the compiler by procedural macros.

await!, even if you allow it to be a macro-like construct with a keyword name, can’t expand to real Rust code. It has to expand to a unwritiable syntax (or just not expand) because it’s its own concept.

It’s currently implemented as a expands-to-yield macro. It cannot be that, as async and generators are orthogonal features (just async is implemented as a generator) and generators as used by await still may never be a surface feature (though it’s reasonably likely).

5 Likes

Honestly, I could live with that. Given that it couldn't expand to the actual low(er)-level code even if it were a keyword, I don't think we would lose anything by not expanding it.

1 Like

It still remains something different from a regular macro in that case, and is a macro-like syntactic construct, even if it goes through macro name resolution rather than being a baked in keyword.

Not expanding is actually worse than expanding to an internal-only representation in my opinion. In the later case, the specialness comes after expansion (and in keyword name resolution), whereas the macro-like construct adds a new impostor to the surface syntax. Just make the internal representation “print” as the macro representation of it and that’s OK, at least.

I would say await is a "real" compiler thing not "just" a macro.

Macro is very powerful, but it has limitation. The book describes macros like this:

Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming.

If some feature cannot be written in existing syntax, it means that the feature is orthogonal to the other language features. My position about those orthogonal feature is that it deserves its own syntax by default, but we can hide that orthogonality in existing syntax if 1) the new feature's use case is similar enough to the existing syntax and 2) inventing a new syntax is too costly w.r.t. the feature's importance.

In the feedback on the Straw poll it was quite clear that most people don’t care about “it’s not a ‘real’ macro”. Many people wrote in concerns about the “field access style syntax” not being a field, and the “method style syntax” not being a real method. But similar comments about the macro style or the postfix macro style syntax weren’t common.

I think this is because a macro is a call to manipulate code at a given location at compile time. Macros are capable of doing a lot, including manipulating flow control and adding early returns. To think of await as a macro is not much of a stretch. (In fact it was prototyped as one) Ultimately an end user does not care if the manipulation of their code is being done by code in the standard library or code in the compiler.

17 Likes

I really, really like @Qwaz’s get_future()..await syntax. It has all the upsides (chainability, using the await keyword, not disguising as a pseudo-field, -method or -macro syntax, not introducing brand new sigils) while having only little of the downsides (The .. sigil already exist in other contexts, but it isn’t nearly as ubique in programming languages general, so it’s less easily misunderstood.) It’s visually remarkably simple and less noisy than the proposed #await and @await sigils. I think it has great balance and should be taken as a very serious contender.

3 Likes

I wonder if the following approach could be a good compromise.

Step 1: Implement the await { } keyword with obvious precedence and required delimiters. This should be straightforward to implement, familiar to users coming from other languages, and useful for cases without chaining. It doesn’t add any new syntax (besides the keyword) and has the least amount of magic. This can happen immediately.

Step 2: Commit to moving forward on Simple postfix macros. Postfix $self.await!() could then be a postfix macro expanding to to await { $self } as described in that RFC. This will take longer to discuss and implement, but will result in a broader improvement to the language. While this is happening, we can see how await {} is used and have more real-world code to evaluate.

Step 3 (far future): Based on usage of async / await, evaluate whether a sigil is justified.

8 Likes

For me, the difference is larger than "who expands the code". There will be semantical differences between await and other macros even though we use macro-based syntax for await. To name one of them, await is context sensitive; It can only be used in an async function.

Under macro-based syntax, those subtleties will likely be explained as await is a special macro that has a few exceptions. However, if we don't use macro-based syntax, await is just a new feature and it's natural to have its own special rules. We won't say "await is a special field access" even though we choose field access syntax.

4 Likes

This cannot be the case, because async is a keyword. You would need to write r#async! to refer to a macro called async defined by the standard library. (The current feature opt-in actually turns off the async keyword-ness to work around this.)

We can't have an async keyword and a plain async macro because if async is a keyword it isn't an identifier.

.. isn’t good because of conflict with typical .. syntax, but you’ve already mentioned that. You also mention that @ or # or ! is arbitrary, they aren’t. people suggest ! because it is already a special signifier of macros, there is precedent here. At the same time, I don’t want to risk parsing ambiguities and bugs with such a symbol, or confusion between macro and not macro (considering it would appear right after a name ie foo!await). @ is also not arbitrary, it is in fact a common symbol used in languages to do “other things” ie in Java @ is used for annotations, in Python @ is used for matrix operations, heck in matlab it means it is a function handle. I even looked up to see if @ was a special symbol in Ruby, and what do you know, it is! @ is a very common “special” signifier. The only downside is that it may conflict with pattern binding, and the use cases may be similar enough that it would introduce too much work, or not be compatible with current parsing. # is an alternative with the same use cases, and is only used in a select few scenarios as a prefix outside of a block, it seems like it could work as a replacement.

.. doesn’t signify “special” it is used way too widely, and the ultimate goal of these types of sigil syntaxes is to provide a general method for postfix special-ness which could be applied with yield or other things, and while .. might work fine some of the time for await (outside of a for loop any way), it may not work for all special post fix syntaxes. The use of a special postfix syntax allows us to use more words, longer words, and more often with out eating up a bunch of words for keywords, and will allow identification of the special case with out needing highlighting.

Ultimately however, .await!() might prove to be the best solution. Even if a user could never implement await as a macro (though other comments call into question centrils assertion they can’t), if it effectively behaves the same, then it should probably use postfix macro syntax, which will be consistent with a potential postfix macro feature, and doesn’t steal keywords or muddy up existing symbol usage.

5 Likes

Its an interesting idea, however the biggest thing here is that now there would be two ways to do something, and what is worse, there is no objective “better” way to do either. How are we going to enforce any types of guidelines here? We may even see minor fracturing because some codebases said await future should always be done, and others said future.await. This would be a nightmare for clippy. And what if people mix both usages? that would be even more confusing. In general “why not both” is not acceptable because we end up making the same mistakes C++ did (in this case, providing N many ways to do one thing, but requiring every one to learn all of them any way because every one does it differently).

(I assume you meant we can't have an await keyword and an await macro)

Is this a case where we can turn off the await keyword-ness solely when used after the . operator, in the same way that union is a contextual keyword?

Even if it does require special casing in the compiler to support both the keyword and postfix macro, it seems like any of the other postfix proposals will require similar levels of magic - but please correct me if this isn't the case.

The important part isn't that a user can actually implement await using a future postfix macro mechanism; it's that they can learn it and use it as if it were implemented that way.

4 Likes

Following the universal pipelining idea I wrote two translations to see how combination of prefix await keyword (without curly braces) and pipelining will work on real-life code:

Initially I was strongly in the postfix camp, but if we’ll get such feature I think prefix keyword with an additional await? sugar will be the best option. Out of 39 uses of await, postfix variant was used only 13 times (also on top of that in 5 cases you could use chaining instead of creating temporaries), so in most cases prefix await keyword is really nice to use. And as many have noted such code is quite “natural” to read and has familiarity advantage. While in minor cases pipelining await allows to write a more compact and composable code.

@Cazadorro

We already have a lot of examples where the same algorithm can be written in two or even more ways. For example you can replace many explicit loops with combinator chains, or you can mix combinators and explicit loops. And I don’t see anyone complaining about it. So I don’t think it’s a bad thing to leave some amount of stylistic freedom, assuming that this freedom is result of combination of orthogonal features which play nice with each other. Some may prefer to create more temporaries, others will prefer to write code in a chaining style. And I don’t see how it will become “nightmare for clippy”.

3 Likes

Algorithms being made in different ways is different than a duplicated syntax structure. This is more akin to the “virtual auto foo()” “auto foo() override” specifier in C++ (or maybe even “this->m_member” vs “m_member”), clearly there should only be one way to do this, yet here we are in C++, with two (or more). What’s more is that clearly this is not a “difference in algorithms”. A different implementation of an algorithm can be understood by everybody whose understood minimum syntax in rust. Two ways to do the exact same thing, the only difference is syntax, is not necessarily understood by all. If someone used the first style, they may not recognize the second, and vice versa. The only way this works is if it works with some other more general system, IE universal macro call syntax, in which case its just balancing between the fact that UMCS exists, is useful elsewhere outside the context of compiler embedded feature, and await!() should be consistent. But then it isn’t using bare field syntax if that were the case.

As for the clippy thing, the team already agreed they didn’t want to implement features in clippy that risked helping segment the language on much smaller issues than this one. It is likely people are going to be requesting clippy has the ability to enforce this as a style guide, either use one form or the other. That’s where the “nightmare” comes from.

And maybe you think, okay, what is the harm in letting one of these features through? Well we could have another feature like this, or two, or three, and all of them stacking up may produce alien code that we don’t recognize (and isn’t just syntactic sugar, but an alternative formulation of the same syntax). Then you start running into the CMake problem, where new code is doesn’t even look the like the same language as old code, and you have significant amounts of people using both, or mixing both syntax’s together with no way to wrangle them in (as it is all allowed and OS’s are sticking to old versions).

I think you misunderstood me. I am not advocating for the original variant proposed by @repax, in fact I am strongly against using . in such fashion. I favor a “more general system” briefly mentioned by you, which is described in the linked universal pipelining idea and which indeed will be an orthogonal feature useful outside of async/await code.

Oh sure, if you had some more general system (even if it wasn’t UMCS) that was useful outside the context of await and also applied here, then that would work for me too. I made the mistake in not looking at your first link before replying, and while some of what I said can apply to any of these systems, awesome language features applicable everywhere > potential fear about duplicated syntax.

After reading the writeup, I’m not convinced about the benefits of postfix await, especially await chains.

First, it’s unclear how often await chains can be used. It is not currently possible to count how many times people use await chains. However, I did the counting for ? chains several months ago (the ability to chain ? has been there for a long time). The result shows that this kind of chaining is very rarely used in the most popular Rust projects like xi-editor, alacritty, ripgrep, bat, etc. Only 15 out of 7066 have multiple ? operators inside a chain like:

xxx
  .f1()
  .f2()
  .f3()
  ...

I even believe people are actively breaking such chains for better code clarity.

Second, the discussion of await chain benefit does not consider how business logic often changes. Currently, we find some code samples like

let player_money = db
    .player_table()
    .get(player_id)
    .await?
    .money;
println!(player_money);

and claim it to be simple and nice. However, it is only simple and nice for the moment being. We all know that business logic changes all the time in the lifecycle of a project, which can quickly make the await chain undesirable. For example, the next day the manager can ask us to implement “trade money for level”:

let player = await? db.player_table().get(player_id);
if player.money > 5000 && player.level < 10 {
    let result = await? decr_money(player);
    let result = await? incr_level(player);
}

In this case, compressing everything into a chain doesn’t provide benefit. Instead, it can be argued that it makes future change to the code more difficult. The variable player is also not so temporary.

4 Likes

Where can we find out more about why we need future design freedom for await?

I see a lot of interest around UFCS macros with await being a macro. I think it'd be helpful if we could compile a summary of why we want it to not be a macro so we can all get on the same page.

I’m 101% sure that errors handling is one of the most bright sides of Rust language, so it can’t be sacrificed - not for better readability, not for somebody’s “habits”.

Also I’m sure that programmer should be able to write nested awaits. It’s a pretty often situation, especially in web dev.

So, if somebody is counting votes here, I vote for what is called Postfix - Method call syntax in discussed document.

self.builder().await()

If some users wants await to be first just to better see it, maybe more simple way is to highlight it in IDE with some bright color. Ability to create nested calls can define future of the language. Ability to notice one keyword in a line can be easily implemented by other tools.