A final proposal for await syntax

A scenario just occurred to me, which I haven’t seen discussed before. I believe .await makes it harder to refactor code. Imagine your line becomes too long or too complicated and that you would like to extract some of the values into local variables. So you start with

return some_expression.await + other_expression.await;

You now refactor this into

let foo = some_expression.await;
let bar = other_expression.await;
return foo + bar;

Seems simple enough. However, there is now an observable difference between the above program and this program:

let bar = other_expression.await;
let foo = some_expression.await;
return foo + bar;

This feels like a big deviation from how Rust programs normally function and I bet that inexperienced Rust developers wont catch this on a first reading since it looks like simple field accesses.

10 Likes

I agree that all of the options have at least one nontrivial downside (which is why this took so long), and I also think you made the best possible choice given that. Moreover, you employed the best decisionmaking process that you could have. (And this despite the fact that, like you, I was in the prefix camp).

In theory it’s strange to have this look like field access. But in practice, I think it’s something that is each developer is going to learn once and never again be confused by it.

But most of all, I’m just glad you’re bringing this to a close. Thank you all for your patience and thoughtfulness. This decision leaves Rust better than it was going in.

7 Likes

There was another reason alluded to in the post:

and making async/await and generators separate, orthogonal features makes it much easier to solve the problem of defining Streams

It would be nice to allow using yield inside an async context in the future to define a Stream. This requires completely hiding the fact that await and yield are implemented using the same underlying transform so that it’s possible to mix the two and get something even more powerful.

4 Likes

This post has good arguments and I am glad to see this reaching a conclusion. If I understand correctly the last open point is which delimiter to use, and there is strong consensus on a period .. The question is then to balance “line noise” – @await has more noise, but is significantly different so newcomers would have to learn what it means precisely – vs the principle of least surprise: would a Rust programmer search for control flow alterations by scanning for await, or for the delimiter preceding it?

If “expression-oriented keywords” such as match (or custom extensions) are a possible future, I guess we’d look for these alternate control flows by scanning delimiters rather than the actual identifier or keyword.

Now the proposal posts concludes on the best bit: async/await needs to ship to enable all this cool stuff in Rust, and the lang team is deciding about that feature and not about pipelining (or whatever people would call this sort of control-flow altering, postfix operations). Still, it might be interesting to see if that’s a potential feature for Rust in order to make postfix-await fit that potential future.

To compare line noise, this repo shows the same code with .await vs @await:

2 Likes

The chaining properties of .await are compelling, especially alongside the .match extension, which would make .await somewhat less of a special case. If we do go that direction, it might also be a good idea to offer the reverse option for await:

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

This gives us the “mandatory delimiters” option at the cost of “more than one way to do it” (which would already be paid by .match, in a sense).

21 Likes

First, thanks to everyone that has been involved in the discussion and the lang team for all their hard work up to this point!

After reading the final proposal, I’m in the camp of being really exited about the decision even though it’s not the idea that I previously advocated. The reason for this is the potential of making the “dot” operator much more general and powerful in the future.

I see discomfort from people thinking about .field as “field access” and .method() as “method access”, leaving .await as “magic keyword”, and I’m sympathetic.

What changed my mind is thinking about the . not as a delimiter like , but as a powerful operator in itself - one that evaluates its operand in a special scope that has the following bindings:

  • All of the fields of the $self object
  • All of the impls of the $self object, curried with $self as the first parameter
  • All of the impls of the traits of the $self object, curried with $self as the first parameter

(I hope I’m using the correct terminology here)

The final await syntax proposal adds the following to the scope:

  • The keyword await, curried with $self as the first parameter

And other potential additions I’ve seen discussed are:

  • The keywords if and match (among others), curried with $self as the first parameter
  • Macros (either all macros or ones specially constructed), curried with $self as the first parameter, allowing $self.dbg!()

These changes potentially make Rust a lot more expressive for authors writing in the method-chaining style. At the same time, they are straightforward extensions to the . operator.

As with all changes, there’s the potential for misuse, but in general Rust’s philosophy on stylistic matters is to trust the author to make the decision as best seen fit.

(As an aside, wouldn’t it be great to have an operator such as -> which could do the same currying but in the enclosing scope, allowing $self->Ok() to be transformed to Ok($self)?)

5 Likes

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.

6 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