A final proposal for await syntax

I feel this way about most "let's just do both" syntax choices, however in this case there's a very clear distinction between when each can be used. If you need chaining, you use .await, and if you don't, you use prefix syntax. This is no different from the style choice of using if let vs .map(): Two ways to do one thing, but which one you pick depends on what you need in that area.

11 Likes

This wasn’t my personal favorite, but you changed my mind. Well done and very convincing. I like the well-reasoned approach of the Rust language team.

I find it interesting that any macro-like syntax seems to be less compelling to people who know the macro system inside out and intuitively understands its constraints. Presumably, for beginners, both macros and await are simply “magic” and fit very well. The language team is at the absolute expert end of the spectrum and intuitively understands that await can never a macro and thus finds these suggestions highly misleading. I’m somewhere in between, as are probably many Rust users.

There’s one part of the argument I’d like to see expanded further, maybe I’m not the only one. There’s this point, already present in the last write-up, there also given without a lot of justification:

we also have consensus that the operator should include the string “await,”

If that’s consensus, there are very likely strong arguments for this that simply weren’t included in the write-ups. I don’t think everybody is already completely convinced that this is necessary, otherwise the operator suggestions (future@ and future#) wouldn’t be as popular as the are.

If one accepts this constraint, however, I think the write-up makes an extremely good case that .await is the best solution.

5 Likes

Congratulations to the lang team for reaching consensus. I am now going to be immodestly satisfied about having treated .await as an operator with unusual syntax (as opposed to field access) before the team officially announced that interpretation :wink: More seriously, de-emphasizing the “field access”/“magic field” aspect of the syntax is a reasonable way to present the feature to the users of the language.

3 Likes

Hey, please understand that we’re all very aware that people feel strongly about this decision - the enormous amount of feedback we’ve received makes that clear. But you’re using very absolute and divisive language to talk about the issue, both in describing your own viewpoint (“fundamentally broken,” “above basic consistency”) and other people’s “no amount of weirdness is enough to make people stop and reconsider”).

This kind of discourse is unhelpful. It escalates the sense of conflict between people with different opinions and drives us toward unhappiness and discord and away from consensus. I’m as guilty as anyone of using this kind of language when I feel strongly about a decision (and to be clear, at points I have felt very strongly against the proposal we’re making now). But this rhetoric only hurts the conversation; for the community to have any semblance of health, we must trust that our concerns will be heard and considered without us framing them as eschatological, and trust that decision makers responsible for each area of the project are acting with prudence even if we disagree with their decision.

50 Likes

I’m happy to see async / await approaching stabilization, and I am really impressed with the lang team’s work on it so far. That said, I’d still like some more clarification on why foo().await? was chosen over (await foo())?. To me, the latter seems much more intuitive and more in line with the general behavior of existing keywords, and I think this benefit is worth the extra cost in ergonomics and line noise. However, the previous post seems to reject (await foo())? out-of-hand, and I don’t understand why that is.

5 Likes

Self-quoting here to emphasize what I see as the difference: I haven't seen anyone claiming that the await operation is a field access. But I most definitely am claiming that it is a method call.

If you claim that it is not a method call, please present a concrete reason why, and if it's one of the two reasons I already presented and rejected, please make a specific argument why my basis for rejecting it is wrong.

2 Likes

It seems to me that a lot of the important variables that dictate what is the best syntax are presently undecided.

For example:

  • There has been some discussion of using @ sigil as a more generalized form of chaining. - If this were implemented it would make that approach much more appealing.
  • Similarly there is an open RFC for Postfix macros. If that were to land it would make that style much more appealing.
  • There has been discussion above of allowing other keywords such a ‘match’, ‘for’ and ‘return’ to also be suffixed following a dot. If that existed the field style syntax is more appealing.

Finally and most importantly, there is the open question of what it looks like in a for loop. We are all still waiting with baited breath for @withoutboats second blog entry on the subject. If ‘await’ becomes part of the loop structure and not the pattern the the simple keyword prefix is more appealing. Where as if it can be made part of the pattern, and can be generalized to things like ‘match’ then any of the Postfix options are more attractive.

There is a large divergence between where the Lang team ended up and what a poll of readers of this form preferred. Perhaps this is a result of an asymetriry in information on the likelihood of these features happening.

Can @nikomatsakis or anyone from the Lang team give some insight into what the current perceived odds of each of these happening are? As they would definitely impact what makes a good choice here. (And conversely this decision could end up influencing which of these other ideas get attention and worked on).

3 Likes

It is not a method call, because await-ing causes the current function to make a non-local jump that can then affect shared mutable state through UnsafeCell without using thread synchronization.

The reason it's not the same thing as a thread yielding is because stackless coroutines have to communicate with each other in a different way. Threads communicate using Sync-hronization primitives like Mutex and Channel. Stackless coroutines cannot communicate that way; they'll deadlock. They can communicate using regular Cells and RefCells, though.

Also, of course, you can't actually implement await as a function, and how it relates to pinning. If you can figure out roughly how it works, you'll know that it can't possibly be a member, but making it look like a function when it actually isn't seems like it might actually be misleading.

1 Like

Yes, but my point is that it's not a matter of feelings here :slight_smile: My arguments have been that the .await field syntax breaks basic consistencies in the language. It introduces new flaws that the language didn't have before. Therefore, I don't see why it can be considered as a proposal in the same matter as the other three ideas.

This is the core of my concern: by putting all four proposals on equal footing, it ends up looking like people are just arguing based on feelings.

Your blog post has a bit of that as well: it explains that .await() and .await!() are bad since await cannot be a method or a macro. It then continues to explain that it's also not a field. With all three postfix constructs being equally "impossible" and needing builtin compiler support, it's strange to see .await being suggested as the best one when it has the least similarity with other Rust constructs that run code and modify state.

Note that async expr is fundamentally different from the three other proposals. It does not suffer from the "impossibility" problem. While your blog post explains that await cannot be implemented as a method, a macro, or a field, it can most certainly be implemented using new syntax. By using syntax, a ton of questions that suddenly become meaningless. I did not see this clearly acknowledged in the Dropbox paper or in the blog post.

I've tried to attack the ideas and to point out flaws in them to the best of my abilities. I think it's very important and necessary to point out that it's not "all the same" and that there are deeper differences between the proposals.

To be honest, I felt disappointed when I saw a bare field access syntax discussed in the Dropbox paper. Some trust was definitely lost there. However, instead of just complaining to my friends, I figured that I could perhaps help by pointing out what I believe are problems with this approach.

It's clear to me that you and the other developers read and think about the points raised here. You've gotten a lot of valuable feedback to take into account.

11 Likes

It looks like more people are bringing up the same point I made here.

I was originally in favour of await foo() (before all of this started). Once the Dropbox post was made, I flipped over to foo().await() because I liked it the most of the options listed on the post. Having said this though, I didn't spend much time considering whether (await foo())? is actually that bad. I'm now at the point where there's so much debate and downside to each solution that I'm not even sure any of them are better than the original problem (or at least the initial version is no worse).

I think it's worth polling the entire community on whether the plan should even change at all. The familiarity of await foo() should not be ignored, and I imagine a lot of people don't care enough about (await foo())? to not have that familiarity. The more we debate and the more solutions come up, the less I dislike what we had in the first place. At the very least, there are no syntactic surprises and it was already the assumed syntax (which should count for something).

Edit: Just to hammer this home a little more, I just came across this blog post in /r/programming and almost all comments offering an opinion on the syntax were disappointed in the conclusion, with one person specifically stating (and yes, unprompted):

I would much rather take (await foo())? rather than foo.await?

I'm also going to cross-post something I wrote on a different thread there, because I feel surprisingly strongly about this.

10 Likes

The other options fall into the uncanny valley. They look like they could be a function call or a macro, but they actually can't. .await, on the other hand, cannot possibly be a member variable, so there's no room for confusion.

4 Likes

Thanks, that's a new way of looking at it for me — and a somewhat backwards argument for why that syntax should be preferred :slight_smile:

2 Likes

At the risk of driving this thread too far off topic, so can a method call.

2 Likes

I'm probably missing something obvious, for which I apologise, but why can .await not possibly be a member variable? (and if the answer is "because it's a keyword", then surely it also could not be a function call/macro?)

2 Likes

Because member access can’t call other code.

2 Likes

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