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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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â.
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.
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.