Await Syntax Discussion Summary

Actually, making it an attribute proc-macro sitting in std could be even better because it wouldn’t force the particular syntax on those who don’t want to use it, but it would still make it possible for proponents to take advantage of it.

Another opinionated Redditor here, reposting my Reddit comment:

I actually really like .await!() - it’s distinct from everything else, and opens up a design space for postfix macros ( .try!() ? .trace!() ? Yes please!). What really worries me is the 1.37 deadline - postfix macros seem like a big, potentially scary feature in their own right, and with a deadline this tight, I’m worried it’ll get discounted based on the amount of work required.

13 Likes

I would also like to voice my support for stabilizing the current form of the await!() macro before settling on an actual sugared syntax.

It seems like there is a substantial amount of people that disagree with the choice of "options to discard" the lang team has made (specifically postfix sigil seems to get quite some love). This leads me to believe that the whole syntax-case feels rushed and that we might settle for something sub-optimal here.

The writeup mentions that there is agreement on not taking this path, but I don’t see elaboration on why.

I, too, would like to see why this choice was made. I'd rather stabilize async/await as a language feature and then settle for a final syntax than the other way around. If you think about it, that's also exactly what happened with Rusts error handling story. The fundamental traits and structs have been there for a long time before the ? operator was introduced.

7 Likes

Can’t read the document at work, but was a general postfix function call operator suggested? A simple sugar for transforming foo.bar().baz?|>await! or something into await!(foo.bar().baz?)? It could be a generally usable operator to be used with single-parameter functions and macros.

How much which argument of the prefix/postfix await syntax weights, also seems to depend quite a bit how await will be used in the field.

I have the feeling that await has to be used a bit more in the field to get a really good idea how to weight the arguments.

Therefore I somehow like the idea of just stabilizing the await! macro for now, and after a bit more experience has been gained, to look again at the await syntax.

1 Like

I didn’t partake in the write-up. Some of my views are congruent with it, some are not, and some things I feel need elaboration. So here are my own views in relation to said write-up.

17 Likes

I'm not convinced postfix .keyword is problematic. The . operator means "do this on the type returned by the previous expression." Syntax highlighting makes it visually distinct, notifying users "this is not a field."

I'm happy with .await, but I suspect consensus on that to be contentious at best.

Yes. Several people have mentioned .await! which is my favourite, along with the "postfix macro" explanation and forward-compatibility. Ideally, empty parens would be optional for all function-like macros, e.g. unreachable! And so .await! would look like, behave like and could be explained as a built-in postfix macro, regardless of whether they're added as a general feature.

Hypothetically, a postfix macro would take "the previous expression" as input, transform it and can modify the control flow, which matches the behaviour of .await!

I agree with Centril's views, especially about composability and chaining. I'm also happy with postfix sigils, e.g. foo()#? or foo()@?.

We can be forward-compatible with postfix macros without deciding whether they'll be added to the language. This is not a blocker.

10 Likes

I don't think we should rely on syntax highlighting while designing language features.

8 Likes

Note that .await!() cannot be implemented as a macro in the literal sense (details, see https://github.com/rust-lang/rust/issues/51751 and https://github.com/rust-lang/rust/issues/51719) . It would need to be special cased in the parser and to keep await as a keyword it cannot even interact with name resolution.

5 Likes

However, you are not forced to chain just because you can. In my view, being forced to introduce names for temporaries that are not semantically important is not helpful. It makes users introduce bad names. Naming is a hard task in general and users should not be forced to do if it it isn't necessary. I think forcing temporary let bindings also makes users keep more things in their head (because there are more bindings in scope that can be used). Moreover temporaries encourage longer functions, due to the increased number of bindings. I generally believe that we should try to facilitate short functions and discourage longer ones.

Temporaries can increase the readability of code, even if there's no need for them, by highlighting one step in the computation. The longer a chain the more context you have to keep.

Also there's no simple correlation between the length of a function and its readability.

2 Likes

Thanks to the language team for this great write-up. The rust community is lucky to have you.

I came in with the assumption that a prefix syntax (i.e. the Order of Operations Solution) was the way to go, based mostly on familiarity with other async-friendly languages. After reading, I’m now leaning more towards one of the postfix syntaxes, for the following reasons:

  • Orthogonality.
  • The strongest case for the Order of Operations Solution as I see it is that it is familiar to many programmers already. But I would argue this familiarity transfers well to any other syntax that uses the “await” name (as opposed to a sigil syntax).
  • I found the “Weirdness Budget” argument persuasive, and think it should be a main factor in the consideration. However, I worry that optimizing for the lowest weirdness cost today may end up causing more weirdness in the future. Because postfix syntax seems to compose more orthogonally with other language features, it seems less likely to conflict with future changes to the language.

Among the suggested postfix syntaxes, the .await!() postfix macro style stands out to me as the most interesting, assuming the concept of a postfix macro could be made more general.

  • Because a postfix .await() method call syntax does not behave like a normal method, it may be confusing to users. A postfix macro call communicates the behavior more clearly.
  • If .await!() could be implemented as a standard postfix macro without special keywords or behavior, that seems like a win to me. Maybe that even opens up the possibility for library authors to build upon it in the future.
9 Likes

I generally disagree. A temporary, unless quickly shadowed, adds to the overall state of the scope and all descendant scopes. In particular, if you add a temporary to the top scope you add the most state. This becomes even worse when the whole function does not fit in the screen when reviewing.

That's true. However, on average, chains are simply not that long to amount to a notable amount of context. A chain also builds upon the thing it is chained on, thus reducing the speed of context build-up. Partially because of this, iterator chains are in my view a good example of conveying a lot of information quickly in a way that can be understood quickly as compared to a version with a lot of temporaries.

Simple? No. I do definitely feel that long and especially deep functions are harder to review and understand. Splitting up functions also lends itself to breaking things up into logical units such that you don't have to fully understand the implementation details of the called function.

5 Likes

I feel like the . operator means "access this property of the type". This is not exactly the case in Rust, but it's close enough to generally not matter. .await then becomes really special, and not in a good way - it looks like it could be attribute access, but the attribute name itself is "magic".

It's not just specification/stabilization work that worries me here, it's also the actual implementation of it in the compiler - AFAICT there's basically no support for anything like this in the compiler anywhere, so it'll have to be built from scratch.

1 Like

Has there been a proposal for an Await trait?

I have googled around and can’t find any indication if there has been any consideration for await as a new trait in the standard library. If there was a new Await trait with an await method, then we get postfix notation without any weirdness because it doesn’t have to be a keyword anymore.

Also, I think that an Await trait could have a trivial impl for anything implementing the Future trait.

3 Likes

Nice document and nice discussion. Thinking of Niko’s “third way” recent blog post, here are some random thoughts.

Postfix macros would be cool in general, but I believe await should be a first class feature that does not rely on macros (to provide good error messages).

In general if we had postfix macros, prefixed by (e.g) @ instead of postfixed by !, we could have foo@ok (similar to a reverted ? that early returns Ok) and foo@await (extra parameters optional). That would open new possibilities in the design space for cool, 3rd party language extensions that play well.

Back to await. I’ve used the await! macro for 6 months now and mandatory delimiters work fine, however the postfix syntax seems just as usable and would indeed help remove a bunch of parentheses.

I would really prefer if the . sigil was not used though. Java uses .class as a special field that returns the class and I found that feature very confusing the first time I encountered it, even though it was not altering control flow. My experience in other languages with synthetic fields is that they never stop feeling weird.

Long story short, I think the solution chosen should:

  • favor orthogonality (no baked in interaction with other features)
  • not look like a field access or a method call
  • be first-class / not include extra disambiguation sigils such as ! in macros.

If postfix macros are to be a thing one day, @await could be first hardcoded and then retrofitted as a postfix macro with custom error messages.

About single-sigil postfix await, I think they make sense. I hear the argument about the strangeness budget and a big reason I never wanted to really learn Perl is that the code looked like a monkey had repeatedly hit the keyboard.

At the same time, if Rust is to become a strong contender in async programming (and it’s bound to be) it really makes sense for it to have a dedicated sigil / shorthand to make the code as close as possible to blocking code. No one ever accused Erlang to use ! for message sending (or that was long ago).

4 Likes

I think the issue here is that await in pretty much any form requires fancy codegen to work (because it also becomes a yield point, etc), so making it a trait method won’t work.

What if the trait method await was just a wrapper around whatever other thing is necessary for codegen (such as the existing macro)

1 Like

I’m very much not into this, FWIW. It’s spooky action at a lot of distance.

1 Like

Although at the language level it'd look and behave like a postfix macro, it would not be implemented that way. As far as I can tell, .await!() or .await! would not add significant implementation work over any other await syntax. See Centril's comment:

4 Likes

Oh yes, I know, but at least there's framework in the compiler for prefix operators and such, and .await!() is a very new kind of syntax.