A final proposal for await syntax

Right, thanks.

There seem to be two slightly different arguments here, which I think is what confused me – one is that the syntax is potentially misleading to people who don’t know what await is (and therefore that the method call or postfix macro syntax would be better, because it indicates that something substantial or unusual is happening), and the other is that the syntax is potentially misleading to people who already know what await is (and so the field access syntax would be better, since to them it obviously isn’t just a field access).

4 Likes

If it can’t be .await() because that would confuse people into thinking its a function when it “clearly couldn’t be a function because of its behaviour”, how is .await somehow better? I would assume the same argument applies to things that look like field access.

Aside from that, it just feels really odd that considerations like consistency with existing semantics is being put behind everything else. Sure this proposal works, but its also very much failing the “principle of least surprise” test. Newcomers would be confused unless they searched it or happened to read about async in rust. People who use the language in general have an additional burden of checking if .await appears somewhere in a chain of things to see what their control flow is.

Something that other languages have done is to purposely split up suspend points so that it is immediately clear to the reader and writer what the control flow of the code is. Generally this is done with prefix await but I absolutely understand why that is undesirable to rust. I don’t come bearing a generally better idea, but I believe this is an extremely important part of async (and generator) functions.

6 Likes

I don't have a source for this, but I'd assume an additional prefix option can have advantages for screen reader usage as well? Then you go into the expression knowing there's an await point. Maybe someone with more experience here can give input as to how helpful prefix control flow is in that case.

It would also allow bringing back the easier-to-find advantage for projects that do reviews via diffs where syntax-highlighting might not be available.

5 Likes

One downside with using prefix keyword + postfix macro is that in situations where the macro form is used you can no longer rely on await points being highlighted as keywords. Where as presumably in the postfix keyword variant await would stand out from methods, members, and theoretical postfix macros, if you make it uniform sugar you’re back to having to specifically recognize the await identifier itself, because its highlighting would be that of a macro.

2 Likes

Phew, I see a lot of discussion about alternatives here, so I wanted to note that I’m actually quite excited about postfix await and I think the rationale from the lang team is very clear – I love these kind of background explanations! Looking forward to playing around with this soon.

11 Likes

I can totally empathize with someone that has the not-unreasonable mental model of .field_name as "field syntax" seeing this proposal as a strange inconsistency - after all, with that model, the only way to make sense of .await is as a magic field accessor, a one-off property getter with strange control flow.

But from what you know of the lang team, are they really the type to pick an ad-hoc solution just for the sake of being weird, or just to shoehorn in a feature? Everything I've seen from them suggests the complete opposite. The Rust way is to take a deeper look at the problem and solution space and come up with an innovative approach that is better than what's been done before.

In this case, the real underlying issue is that sometimes one prefers keywords in prefix position, and sometimes one prefers them in postfix position. If you have to pick only one variant forever, you end up penalizing the programming style that isn't favored. Also, there isn't much to go on as far as how to implement postfix keywords in any existing language.

The insight that was hinted at in the final proposal (and has been brought up a few times in related discussions over the last few weeks) is that the solution may be to loosen the restrictions on the . operator that we've inherited from other languages. Currently, keywords and macros aren't allowed as . operands, but in most cases it's pretty clear how they would behave if they were.

This requires changing one's mental model. One way is to add a new rule: .await is a special case. A different way is to start thinking of the . operator as a the "chain" operator and remembering what isn't allowed to be chained.

Yes, this adds to Rust's "wierdness budget", but this is doing it for the right reason: discovering a way to solve a problem that many programming languages have, but few have really tackled.

21 Likes

Thanks, boats, for another excellent post!


I'm now really amused by the idea that ? is actually just sugar for :laughing:

(For extra clarity, just in case: I'm definitely not actually proposing that the latter be made to work.)


loop { x } and r#loop { x } also both exist and do different things.

This kind of thing is why raw identifier syntax exists, so I don't understand the argument that it's a problem.


I'd like to point out that the paper writeup for the previous syntax thread explicitly said

For that reason, we have excluded sigil based syntaxes like @ from further consideration. If we adopt a postfix syntax, it will include the await keyword in some way.

That didn't stop sigil-based syntax from being proposed in the thread anyway.

If you have suggestions for better ways for teams to communicate the state of things, I encourage you to give feedback to the governance working group.

6 Likes

I have a friend (who unfortunately doesn't seem to be participating directly in these threads) who suggested that discouraging unlimited chaining of asynchronous operations would be a benefit of prefix syntax. Together with devsnek's point, that actually seems fairly reasonable to me.

I think that this is an extremely important consideration, and crucially, users of screen readers could easily be unrepresented in a discussion like this. (And, of course, it undermines the assumption that IDE features such as syntax highlighting can clarify otherwise-unclear syntax.)

8 Likes

I’ve been very loosely following the discussion, but (like many other people) I was confused why expr.await was the most promising candidate, given the inconsistencies with field access and with other competing languages. However, the prospect of generalized expr.keyword support changes that entirely: if I can say expr.match {} and expr.if {} else {} are possible, then expr.await would be natural and consistent; and if match expr {} and if expr {} else {} are possible then await expr should also be possible, and would be consistent with other languages.

My only concern, then, is with the order of operations. If Rust stablises async/await support with the await expr syntax, but some obstacle prevents stabilising generalised expr.keyword syntax quickly, then Rust will be stuck with a suboptimal await syntax… but at least it will be a consistent and familiar one. On the other hand, if Rust stabilises async/await support with the expr.await syntax and some obstacle prevents stabilising generalised expr.keyword syntax (including the await expr back-formation), then Rust will be stuck with a quirky special-case syntax.

In order to get async/await stabilised and usable quickly, I think it makes sense to pick a bland and unadventurous placeholder syntax like await expr, and then work on stabilising the syntax we actually want with generalised expr.keyword.

(as another vote in support of generalised expr.keyword syntax, note that rust-analyzer already offers such things as completions and rewrites them to the stable syntax)

12 Likes

I agree, as I don't really "get" the chaining argument. Take this basic code:

let mut frob = Frob::new(Widget::open()?)?;

Is this idiomatic, or is it better to split it over two lines?

let widget = Widget::open("")?;
let mut frob = Frob::new(widget)?;

I find the second one much nicer. And if we agree on that, why the push for

let mut frob = Frob::new(Widget::open().await?).await?

?

Perhaps I have a limited view, but I think (non-stream) async calls as being of two kinds:

  • connect-style operations, like on a socket, HTTP connection, database, device discovery API, my Widget::open() example and so on, where the result of the future enables access to new functionality
  • transfer operations, like message reads or writes

For the first kind, it's often desirable to reuse the "connection". Even when that's not necessary, it's not a bad idea to avoid chaining unrelated operations like establishing a connection and transferring some data.

For the second kind, these are often "one-shot" (wait for a timer, or open a connection, send a message, maybe wait for a reply) or involve looping (accept a new connection, spawn a handler for it, start over). For the former, chaining isn't bad (although it's totally fine to avoid it), and it's not applicable for the latter.

So I'll argue that for run-of-the-mill code (proxies and gateways, CRUD/REST services), chaining is not a requirement, and often not desirable. And when it can be used, it's often very limited; take a look at https://github.com/inejge/await-syntax/blob/postfix-field/bin/wlan/wlantool/src/main.rs and count the chains of three or more awaits in a row.

This means that there is no need to introducing new syntax solely to support chaining.

4 Likes

By the same logic:

You can define a function r#return:

fn r#return(_: i32) {}

And you can call it with r#return(x). However, return(x) also exists and does a different thing! This is weird and inconsistent, because r#foo(x) and foo(x) are equivalent for (almost) all other function names since the raw identifier quoting syntax is optional for normal names.

...In reality, it's not inconsistent. Keywords behave differently from identifiers; that's why they're keywords. The r#foo syntax to get an identifier named like a keyword is essentially a backwards compatibility hack; it mainly exists to handle the case where you're using a crate that was written in an older edition of Rust and defines an API with a name that has become a keyword in newer editions (for example, await became a keyword in Rust 2018). The entire point of the syntax is to suppress treating something as a keyword, so the fact that it behaves differently is not surprising. Although r#foo can also be used where foo is not a keyword, in which case it behaves the same as plain foo, that's not the intended use.

The only "inconsistency" is that await would be the first, and so far only, keyword that can appear after . in Rust. That would make it unique, but that's not the same as inconsistent. And as mentioned in the post, there is some thought of allowing other keywords in that position in the future, such as match. Also, there is plenty of precedent from other languages for allowing things other than field or method names after .. For example, C++ supports foo.operator+, foo.template bar, foo.*bar, and foo.MACRO (where MACRO could expand to anything). I'm not saying C++'s syntax is a good role model, but I think .await would be a good deal more principled than any of those syntaxes. :slight_smile:

11 Likes

Thank you — you sum it up very well.

Personally, I've never preferred a keyword in a postfix position since it isn't a thing. I've never wanted to write

foobar.return;

instead of the normal return foobar;. When should I use which? Similarly, I've never wanted to write

pred.if { ... }

or

elements.for { ... }

It's not even clear to me how the postfix for construct should work since the syntaxtical construct has two parts: a pattern expression and the iterator expression.

How far does this syntax go? Should

foobar.fn { ... }

be a thing? Why not?

Yes, having keywords in a postfix position would open up new possibilities and making this work for more keywords than just await would make the language consistent in this regard. Still, I believe we'll be opening up a whole new can of worms if we go into this direction.

Exactly. A well-known and well-understood syntax will make the language more expressive with minimum risk. Even stabilizing the await!() macro would do that.


Yeah, true, raw identifiers allow you write fun stuff like if r#if { ... } and if r#false { ... } where r#false is true :slight_smile: However, my point above was a little more quirky: .await would make it the first time that Rust puts a keyword into a position where we've so far only had identifiers. Since keywords can be used as identifiers with the raw identifier syntax, we have a new kind of clash here.

But I concede that it's probably not of great concern — raw identifiers were bound to cause this kind of clashes.

11 Likes

Yeah, you've told us so many many times already...

return isn't eligible and you know that because you've read the proposal:

[...] potential future extensions in which some “expression-oriented” keywords (that is, those that evaluate to something other than ! or () ) can all be called in a “method-like” fashion.

14 Likes

It's pretty easy to distinguish this case because in fn foobar(), foobar is not an expression.

As for if and for... dunno. Personally, strangeness budget concerns would make me lean against them, but for better or worse elements.for is quite reminiscent of Ruby. [edit:] And .if would be similar to the C ternary operator.

3 Likes

I couldn’t agree more. The whole await syntax discussion seemed a bit disproportionate, putting too much importance into the await syntax by trying to micro optimize every single use case.

2 Likes

Sorry, I'm not a language expert and don't have a clear understanding of which keywords are "expression oriented". For me it seemed that return foo would very operate on foo, similar to how match foo { ... } operates on foo.

So in practice, I guess we're only talking about .if, .match, and .loop? Do we have other expression-oriented keywords?

I tried looking it up and found the ExpressionWithBlock term on Expressions - The Rust Reference — which seems to confirm the above list with the addition of the unsafe keyword.


Thanks, makes sense and matches the above.

I really have a hard time following this argument.

Yes, consistency is a great value, but just allowing every keyword (match, if, …) to be postfix doesn’t quite remove the strangeness of a postfix keyword, it IMHO just opens the door for every different styles of writing code in Rust, which won’t quite increase the readability of Rust code.

7 Likes

Thanks for all the hard work on this issue, both with implementation and syntax issues!

There is one other alternative that I think would be reasonable, in addition to what has already been discussed. The reasoning for not having await be a postfix macro seems valid, since it is quite different from previous language “macros”. But what if a conservative keyword such as await {x} could be added to the language so that x.await!() could be thought of as implemented using that keyword (in a world where postfix macros are valid and the keyword doesn’t collide with the macro :stuck_out_tongue: )?

1 Like

@kornel made a really good point in another thread that the dot operator can already be seen as syntactic sugar for the (prefix) universal function calling syntax:

Supporting both await expr and expr.await seems consistent with this model.

4 Likes

I agree. I’m admittedly new to the discussion and a non-expert, but it seems to me that await expr is uncontroversial and almost inevitable; either as the initial syntax or after expr.await leads to expr.keyword, which leads to await expr for consistency’s sake. The proposal is solid, but I think it would be nice to have the more familiar alternative first and handle the general expr.keyword case in one go afterwards. Either way, thanks to the team for their work on Rust!

6 Likes