A final proposal for await syntax

Thanks for the summary and continued work on this! I previously argued for foo.await() and foo.await!(), so seeing them leave the table is disappointing but understandable. Given all of the constraints and suggestions in the previous thread, I agree that foo.await becomes a logical frontrunner from the initial set of solutions.

Now that we have a tentative conclusion, I’d like to raise a final thought or two, even if the ship has potentially sailed:

The initial problem was one of ugliness. I personally feel that this new syntax isn’t much better in terms of ugliness, and it also introduces potential confusion for those unfamiliar with async/await (which is not a small amount of developers). I don’t think that this is much better than the syntax we’re trying to “fix”, especially when you consider that manually unpacking the result on the next line is possible. For my reference as much as anyone else’s, here’s what I see as a comparison (please correct me if mistaken):

// not checking result
let bar = await foo();
let bar = foo().await;

// using '?' operator
let bar = (await foo())?;
let bar = foo().await?;

// manual unpacking
let bar = await foo();
let bar = bar?;

As the issue at hand is pretty much just ergonomics, I believe we should officially poll to see if people prefer foo().await? or (await foo())?, rather than just do the eye test on a thread in the forums. This poll would be very specific (X vs. Y) and so it can hit the broad community without much overhead, allowing us to choose which is deemed more attractive by the majority of users. Although I do feel we should show both options with and without ?, to avoid bias. In the case the poll is split down the middle, we can then defer to the language team to break the deadlock.

Making ergonomic decisions based on a couple of forum threads where basically everyone disagrees with each other doesn’t feel like it’s a path to success, IMO. Although any replies on the internals threads are obviously valuable, I feel there may potentially be a chance that the results are skewed. For something as prominent as this feature, I don’t know if that’s something we should ignore.

tl;dr: is adding this new syntax actually better enough than what we started with to warrant doing it?

5 Likes

I wonder why not? When we designed language features in C#/VB, we saw that users learnt from syntax colorization extremely well, and quickly - to the point where colorization was a central part of the design of new language features.

For instance: in C# you can write “int x = 15” to declare it as type int, and then later we introduced “var x = 15” to let the compiler infer its type, and for back-compat reasons we couldn’t reserve the word “var”. Nevertheless because it was colorized as an operator, no one would even dream of using it.

For instance: with string interpolation, the different color of text inside the “expression holes” is what allows users to reason about whether or not they’ve correctly declared an interpolated string or not. If we couldn’t rely on that, we’d have had to invent a more heavy-weight syntax for users to be sure that a string expression allowed interpolation.

12 Likes

I understand this position, but I hope trade-offs will be explicitly discussed at the May 23 meeting. In addition to a lesser semantic load on dot operator, a separate sigil will open a straightforward path to functions pipelining, and will follow principle of least surprise much closer (thus being easier to learn), so I believe it’s worth considering.

For example sometimes I have to read code without any highlighting, e.g. in console. And Rust is different from C# as it does not have a single “blessed” IDE/development environment.

6 Likes

A big thank you for your calm and well-justified position - as expected of the Lang team. It seems that dot async is a very balanced choice with the least downsides among the other syntaxes mentioned. Also, the possibility of other postfix keywords is intriguing.

One thing that I’d like to get a confirmation of is whether sigils other than the ones mentioned in the blog post were given thought? A proposition of using double dot: expression..await was mentioned in the last thread, and compared to # and @ it seems to have much less “line noise”. It does repurpose an existing sigil, but that is a point that is shared between all other proposals, including the single dot. @mgeisler 's observation that unlike field access, await syntax is not cleanly “reorderable” because of it’s side effects, warrants having some visual difference between them. Between the sigil choices, a double dot seems to me to be the closest to the “single dot base case” and the most noise-free one that fulfills the criterion of visual dissimilarity.

2 Likes

First of all, thanks to everybody who contributed the await syntax proposal :+1: . I can definitely see what makes a general postfix operator compelling. I do support what @rpjohnst wrote, to then also make prefix and postfix operators interchangeable (if dot-await will be implemented):

Prefix Postfix
match match x {} x.match {}
await await { x } x.await

But for me it ruins a bit the clean cut between field access and method call that is currently present. Method calls on fields (some.field)(123); are also clearly and visibly distinguishable. With .await one needs to know that this is not a field access but a special case where magic will be involved behind the scenes. So my two cents on this topic would be to alter the syntax a bit to make it clearly distinguishable from a field access or method call. Maybe .await! (dot-await-bang) / .match! (dot-match-bang)? Or, when comparing the futures situation to how the solution for results looks like, one might think of a single !, like:

let from_result = foo()?;
let from_future = bar()!;

let from_future = bar().await!;
let from_match  = some().match! { .. };

Thanks :smiley:

2 Likes

I really like this: it’s consistent, it provides a viable chaining postfix syntax (.await), and in less async-heavy code normal await works the way other languages do it.

The mental model here becomes “Rust has await syntax like other languages, but there’s also a cool bit of sugar for it.”. To me this is much nicer than “Rust has a new kind of await syntax that I have to learn”.

This solution of offering a prefix operator and postfix sugar also works well for a postfix macro based system! We can offer both:

  • await foo
  • foo.await!()
  • (potential future extensions to allow user-defined postfix macros, which folks want anyway)

where the macro expands to the expression (and is a true macro, getting rid of the issue in the original post about this basically being not-a-real-macro and confusing).

Personally I prefer going the macro route but that’s mostly because I have felt the need for postfix macros (mostly for guard let-style situations) more than I have felt the need for postfix match or if. Others may have differing experiences.

Overall I feel like we should be open to adding multiple syntaxes (one prefix, one postfix) here. Not as a compromise solution, but because with multiple syntaxes the mental model genuinely seems to be better: there’s the syntax everyone is used to (prefix await), and a sugar syntax that’s better in some scenarios. This seems to address most concerns against .await that I’ve seen, and all the concerns against .await!() from boats’ post. We still have to pick one postfix syntax, but it feels like the outcome would be more agreeable to everyone, and have fewer downsides.

35 Likes

I’m immensely happy that we have a final proposal. I was a big fan of postfix sigils, especially foo()~, but I am sure everyone will adapt to foo().await just fine.

I just find it amusing how different the preferences shown in the straw poll are from the final proposal. But this is well explained in this blog post: people want new features to stand out, because they are afraid of them. Instead, their syntax should resemble Christopher Alexander’s buildings: perfectly normal and completely “in place”.

2 Likes

I was initially a proponent of the prefix await operator, mainly due to its parallels to other languages (TypeScript, C#), but after reading the reasoning behind the dot await syntax, I have to say that I’m firmly in support of it.

To echo @withoutboats’ sentiments:

Even if you might have prefered a different outcome on this question, I hope you will remember the bigger picture. Shipping an async/await syntax in Rust will enable a huge number of users and potential users to write highly efficient network services using Rust, a memory safe language. The impact of shipping this feature for our project is enormous, and also (if I might be a little immodest about Rust for a moment) significant for the software industry as a whole. This is the important thing to focus our attention on.

Being able to write fast and safe web services is one of the big draws for me to Rust, and having async/await stabilized is far more important to me than the syntax (assuming the syntax is sane, which I think all of the potential options put forth have been).

6 Likes

I think that the possibility of future postfix keywords (which is a great idea, and await being postfix is a fine decision!) should be a reason NOT to settle on the syntax for such postfix keywords/sigils now. Obviously it’s a contentious topic due to dot sigil being so overloaded (and it is, I’m 100% for a different sigil). Locking us into it before the topic as a whole has had much work done on it would be a mistake. As @Manishearth proposed, perhaps it’s best to leave it as a postfix macro, since atm postfix macros are not a regular feature. Then it becomes obvious that this is currently not a macro, but in the future when a postfix keyword syntax is fully settled beyond just await and when postfix macros arrive, it’ll just be a plain macro desugaring to the final syntax.

tldr; postfix good, yes, but maybe don’t lock in sigil as dot when future keywords would also use it, before alternate, but still not “line-noisey” sigils are tested out.

5 Likes

I have read @withoutboats’ post, I understand and appreciate where the team is coming from, and I can live with the decision being postfix .await, but I feel I must push one more time on the “no, really, await is a method” (and therefore postfix .await() is the most logical choice) perspective. If I still can’t convince people of this I will drop it.

I’ve seen people put forward two different reasons why await is not a method, and I find neither of them convincing:

  • await has atypical control flow behavior for a method call. To this I can only say, again, no it does not. await is not like ? or break or return; it cannot cause control to transfer to another point within the local control flow graph. It is not like fork or setjmp; it does not return twice. It is not like panic! or longjmp; it cannot directly trigger unwinding. It isn’t even like if or match or for; it can’t even make the CFG include a branch.

    The control flow graph for code that uses await contains one forward edge into the await, and one forward edge out of the await, just like any other function call. Entering the await does not guarantee you will leave it again … also just like any other function call.

    Yes, there is a bunch of complicated control-flow stuff that an async fn or block does under the hood at the point of each await, which you either shouldn’t need to be aware of at all, or it’s better to think of it as a property of the async construct. I feel that hiding that stuff is perhaps the most important benefit of the async/await feature, and so I am very much in favor of surface syntaxes that reinforce an appearance of there being no magic at the point of await.

  • You cannot implement await yourself. Well, it’s true that you can’t implement the complicated control-flow stuff that I handwaved over yourself, but I think that’s better thought of as “you cannot implement async blocks yourself.” All of the behavior that anyone who is merely using async/await, and not implementing an executor for it, needs to be able to implement, they can implement, using the Future trait. The method you implement, AFAICT, is called poll, not await, but I don’t see that as being any different from how implementing From<U> for T means that people can call U::into(). It just means there’s a layer in between, and of course there’s a layer in between, because the executor needs to do some stuff first.

By contrast, I do find the argument that .await is confusing because it’s not a simple field access to be somewhat compelling. I have written a lot of code in Python, where transparent getters and setters are a thing, and I do find they are frequently more trouble than they’re worth. I’m not completely persuaded that it’s a problem — await will be taught separately regardless — but including function-call parentheses after the name of an operation really does help remind people that it can run arbitrary amounts of code before returning to you, and that really can be important when reading code.

20 Likes

I am strongly against this, at least in the short term. This would be very likely to lead to one of two outcomes: either the community would eventually agree (for the most part) that one is better, and the other would be discouraged but difficult or impossible to eradicate; or two “equally good” syntaxes would be used in the wild and we would have a never-ending tabs-vs-spaces style disagreement, where many projects use one or the other style and a few even use both, to everyone’s dismay.

In the long term, it may become clear that the syntaxes are valuable in different contexts and can happily co-exist. But I don’t believe it would be appropriate to stabilize two syntaxes simultaneously.

12 Likes

I’m not sure the whole “await is a field” vs “await is a method” distinction really matters at the end of the day. Despite its appearance, the dot await syntax is not field access, just like adding call syntax to it (.await()) wouldn’t make it a method call.

At that point I just see .await() adding more noise through parentheses.

5 Likes

After all the discussion here, it’s not yet clear to me if it’s really understood that the complaints about a possible .await syntax aren’t a simple matter of preference? It’s not bikeshedding in the traditional sense where we’re weighing several okay-ish options.

No… To me, the idea that Rust should special-case fields with certain names is fundamentally broken and inconsistent with the rest of the language. We’re not debating if the keyword for defining a function should be fn or func, where both options would be more or less the same. Instead we’re discussing putting the desire to use ? above the basic consistency of the language.

As a new example, consider how .await will interact with raw identifiers and how you explain this to Rust developers. Today, field names must simply be a valid identifier and should they happen to be a keyword, then you can even quote them like this: r#for or r#if, etc.

What is the rule afterwards? Well, I guess it’s the same: keywords must be quoted with r#. Indeed, this currently this works on nightly:

struct Foo {
    r#await: bool,
}

You access the field with foo.r#await as well. In a world where .await is a thing, I guess both foo.await and foo.r#await would exist — and do different things! That’s a bit weird in itself and it becomes more weird and inconsistent when you realize that

foo.r#other_field == foo.other_field

holds for all other field names since the raw identifier quoting syntax is optional for normal fields. This is an inconsistency and it’s only there because existing syntax is being misused.

Pointing out such basic inconsistencies should be enough to make the proposal dead in on arrival. Here, however, it seems that no amount of weirdness is enough to make people stop and reconsider.

Reconsider could mean simply stabilizing the existing await!() macro. Yes, it would be a builtin macro that you couldn’t actually define yourself, but the special-case would be very narrowly self-contained. It would be similar to line!() which also cheats – but does to in a way that doesn’t pollute the language.

Also, it’s not it’s not that I find (await foobar())? prettier than foobar().await?… No, not at all. I simply find that the first form fits with Rust as we know it today and that we can introduce it without risk to the overall language design.

22 Likes

I would love for us just to go with the extremely-familiar prefix await (with mandatory delimiters, or as a built-in magical macro, as those are pretty familiar in Rust) for now, and address postfix await and postfix match and postfix macros all together later.

Chaining simply isn’t important enough to dig this deep into the strangeness budget at this point (for example the sample converted Fuchsia code did not look bad at all with prefix await).

23 Likes

A big yes to this. Invoking await is something that will typically block the execution of the current function. It’s something that has a side effect and the very least we can do is to mark this clearly by making it look like a method call. That would make it much more consistent.

5 Likes

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.

48 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