A final proposal for await syntax

(See https://doc.rust-lang.org/nightly/nightly-rustc/syntax/ast/enum.ExprKind.html#variant.ForLoop)

1 Like

Yes that has been brought up in the previous thread, the lang team didn’t want to do it for many of the same reasons that they are against prefix await. (It doesn’t fix the problems of prefix await)

1 Like

To bring this tangent back to await, this looks like a good demonstration that "postfix keywords" are not a clearly defined concept with a single definite syntax/semantics, and in particular, it's not clear how postfix await would fit into this scheme.

More concretely: mostof the .keyword proposals, with the exception of loop and await, are clearly not "postfix" syntactic variants but rather "infix", and there are of course two possibilities for ordering the "arguments" of the expression. The ordering you've shown (putting a conditional expression after the keyword) is the only one that preserves the similarity to the syntax of natural English, which seems like a clear advantage shared with the traditional prefix syntax for these keywords.

Others may draw different conclusions, but to me, this seems like evidence that even if other keywords gain postfix (or infix) variants, that alone may not be a good reason for postfix await.

3 Likes

LLVM is very unhappy about this plan of yours :stuck_out_tongue:

Put more plainly, because for_each cannot return/break/continue, LLVM can actually optimize it better in some cases. A very unfortunate point where for loops aren't actually always zero cost (this is of course not intentional, and can hopefully be fixed some day).

I just want to try and remember that having a postfix syntax is definitely more flexible, because it allows chaining for people that need it, and it also allows us to make a simple macro that turns it into prefix for the people that prefer it. There already even is a crate that illustrates that: https://crates.io/crates/await_macros

2 Likes

Thanks to all the team for the hard thought on this subject! .await is a well-reasoned choice and I can support it.

I just wanted to add my name to those noting the strong precedent of prefix await in so many other languages. This makes me believe it will be much more familiar to new rustaceans than a postfix syntax. I also personally like that await foo reads much more like English than foo.await. (Somebody else already made this point before me.)

Overall I’m in the camp that await foo and foo.await should be both in the language. As many before have already well argued there are readability benefits to each depending on the situation, and we would inevitably establish style rules on when each fits best.

For example:

// For single expressions I much prefer await foo
let x = await foo;   
let x = (await foo)?;

// For chained expressions postfix await seems a much better choice.
// To help readers see where the function may pause I think it would
// be best to put .await on a new line each time.
let x = foo
    .await
    .bar()
    .await?;

If we were to accept both forms, the pipelining proposal seems an interesting (but not necessary) option to connect the two. .match also seems interesting.

2 Likes

We’ll continue to solicit and consider feedback until the May 23 meeting. It’s possible that new arguments will be presented in this time that will seriously change the calculus and cause a different outcome, but users should be realistic about the amount of consideration this question has already received and how likely it is that truly novel information will be introduced at this phase.

We're ~100 comments in to this thread now. I've skimmed and not seen anything I'd describe as "truly novel information" or "new arguments." Have I missed any?

12 Likes

I’ve been giving some slow thought to alternatives. I wanted to ensure I had read through the arguments. I think that everything I’ve read strongly suggests that postfix is best. One of the big reasons for this is direction of eye movement to read an expression. Having keywords before the expression force the eyes to read left to right, then back left, then back right. You read right through the expression, back left to the await keyword, then right again to the next expression.

// bad eye movement!

// finish reading expression
x.async_operation()
                   ^
// return to beginning
await x.async_operation()
     ^
// continue to next method
(await x.async_operation()).next_method()
                            ^

The arguments I’ve seen make a lot of sense. I still am not super enthusiastic about this dot based syntax, though. Ensuring that it is a keyword that could never have a conflict mitigates the problem to a degree, but it still isn’t ideal. It may seem minor, but I want to consider it. One caveat to this is that it requires more up-front memorization on the part of a new Rust programmer. That creates some barrier to entry because they need to memorize this and all future keywords that are created, on the off-chance they try to create a field or method with that name. I don’t feel that is feasible expectation because supporting the Rust world domination efforts ™ ™ requires allowing only partial adoption of the language’s features. It’s natural with programming languages to only use a subset of the features at first. I personally took some time before I started using features of the language. I think this is a step away from that. It’s definitely a small one, but it is nonetheless.

The counter-argument is that syntax highlighting would solve this because the colors would make it immediately apparent that something different is going on. That’s certainly fair, but is it safe to assume that every editor uses syntax highlighting? I have syntax highlighting in my daily editor, but there are parts of my day where I touch languages and editors with no syntax highlighting. I think that’s a rare use case, but do we want to potentially alienate people to whom it applies? A comment on a Reddit post about this also mentioned diffs during merges and pull requests. It’s not unheard of for those tools to lack syntax highlighting for whatever reason. Maybe these are edge cases that are outside the scope of concern

I’d like to reconsider the mentioned alternative of the postfix keyword with a space. I know the straw poll didn’t give it high marks. I also wasn’t a huge fan of it at first, but it has grown on me. Some points in its favor:

  1. It preserves the postfix benefit of unidirectional reading that I mentioned above.
  2. It keeps itself structurally distinct from method calls and field accesses to avoid any possibility of confusion.
  3. A new programmer won’t be tripped up by it if they are completely ignorant of its existence, which supports gradual embracement of language features as discussed above.

There’s the question of whether it has legibility issues, but it doesn’t seem significantly different from the as keyword we already have:’

x as u32;
x await;
x.async_operation() await;
(x as u32).custom_trait_method();
(x.async_operation() await).custom_trait_method();

// versus...
x.async_operation().await.custom_trait_method();

The space option is consistent with the as keyword here. Technically as is infix, but it behaves similarly to postfix in that it comes after the primary expression of interest. It doesn’t require a programmer’s eyes to change direction while reading the code.

This could open up interesting possibilities when used in conjunction with try blocks or with the as keyword itself if it were ever to be extended.

try {
    [...]
} await;

// hypothetical below!

try {
    [...]
} await as SomeMagicType;

x.async_operation() as SomeMagicType await;
x.async_operation() await as f32;

Which seems more elegant to me than:

try {
    [...]
}.await;

This may be worth considering. I like its postfix nature so the reader’s eyes don’t need to change direction while scanning code. I like its similarities with the as keyword. I like that it is distinct from existing Rust constructs/idioms like field accessors. It keeps it unique as a true keyword.

I recognize that it might not feel as clean if you are putting a lot of await calls in a single line. The same can be said for any “single line” that contains a lot of chained methods, and usually that’s when I start to break things apart into multiple lines for legibility anyway. Some of this feels like a collective appeal to extremes. Sure, it’s possible that huge messy situations could occur, but (maybe my use cases are not representative of others’) I can’t imagine a likely use case where I would want to chain a significant number of awaits and method calls together where I wouldn’t/couldn’t break them apart for clarity anyway.

Additionally, if you for some reason had Future<Future<Future<T>>> you could just do x await await await; without issue. I think the only concern then would be ugliness around people opting to not use the parentheses for clarity:

x.async_operation() await.next_method();
// instead of
(x.async_operation() await).next_method();

I certainly prefer the latter, but I’m not entirely sure the former is completely terrible in this particular example. The former seems to flow somewhat in saying something like “x run async operation. Await then next method.” If you imagine it saying wait instead of await, then it might be a hair more intuitive - x.async_operation() wait.next_method();.

That being said, I can see it has a potential to get messy. That can happen anywhere where people try to save precious bytes in their source code by exploiting obscure operator precedence rules. I’ve seen it in every language. In my experience, Scala is particularly prone to taking this to the extreme. I don’t believe we’re in danger of that here, because our situation are still limited to defined keywords.

I recognize some will still think that .await is better. Yet I propose this as a middle ground which attempts to address the concerns of those vehemently opposed to the dot syntax while preserving the advantages highlighted by those in favor of it.

1 Like

If space-await were kept fully consistent with as then this wouldn't be an issue, even though a . cannot be part of a path as doesn't currently allow following the RHS path with a method so the parentheses are necessary:

error: expected one of `!`, `(`, `::`, `;`, `<`, or `}`, found `.`
  |
  |     5u32 as i32.abs()
  |                ^ expected one of `!`, `(`, `::`, `;`, `<`, or `}` here
1 Like

In general, I’m happy with the decision for a postfix approach.

That said, I wonder if future.await() wouldn’t have been a safer choice, for the following reasons:

  • It’d be straightforward to allow parameters later on, e.g., an explicit waker or executor
  • The syntax future.await (i.e., without parameters) could then allow us to refer to the generated closure, without actually calling it.

It would also IMHO be easier to teach, as a “coroutine call”, which essentially it is.

If it turns out, we never add a parameter, or refer to the closure, then allowing the parentheses to be dropped would be a comparatively simple option (while adding parentheses afterward’s wouldn’t be).

1 Like

Just a small thought, and taking a small step aside… what about .then value { ... } in addition to .await?

Ah, this is a good point. We don’t have to worry about that then. We could still allow the programmer to use the ? operator on it since other operators can be used immediately afterward, so we wouldn’t have to put parentheses everywhere.

x.async_operation() await?;
x.async_operation()? await;

I guess that raises the question of why we would end up allowing a dot after the question mark in the above scenario, but that seems like a solvable discussion.

x.async_operation await?.next_method();

That doesn’t seem terrible to me. I still like it because a new programmer doesn’t need to know that it exists in order to avoid it.

1 Like

await is a keyword anyway so it's still impossible to call a field or function await. It's not necessary to know or remember this because the compiler will remind you. :slight_smile:

3 Likes

I don't think the total number of comments is quite fair as a metric for how much "noise" is being generated without new arguments, because a lot of the comments are clearly not written with the intent of changing the team's mind.

I do, however, think that several points made so far seem fairly novel. I'll quote them here without introducing my opinions on whether they're valid or important; they're merely included because they weren't mentioned in the team's write-up or in the previous internals thread.

.await makes + non-commutative in some cases:

.await() should not be dismissed as "not a method":

There is some circumstantial evidence that the community may feel strongly against this proposal, which may warrant additional outreach and research:

Chaining may actually be detrimental to clarity:

.await may be detrimental to accessibility:

There is a proposal for a generalized "postfix (or infix) keyword" syntax that would not apply to .await:

An argument for .await that wasn't in the write-up:

5 Likes
"test.txt" File::open?

Rust is not Forth.

2 Likes

I don't see how this is .await-specific. (await foo) + await bar and (await bar) + await foo are also different.

(Adding parens there because I don't intuitively know what the relative precedence of prefix await and infix addition are.)

I've heard both of these in various forms, but not why await is substantially different from ?, which is also about "the control flow of the code" and whether you "go into the expression knowing" there's a rethrow point.

Personally, my default is to apply higher weight to arguments that also argue for previously-accepted things and lower weight to arguments that argue against previously-accepted things.

This was in the original write-up:

5 Likes

I would like to point out that there is no guarentee that + is commutative in general, and in some cases will not be, for example string concatenation. So the objection that await makes + non-commutative is not well founded.

7 Likes

This comment seems to be of satirical nature but I can honestly imagine using it sometimes if such postfix function application had an appropriate syntax. For the moment, the best comparison where this is subjectively more readable. Imagine opening an adjacent file:

// Instead of:
File::open(path
    .parent()
    .ok_or(Error::NoSuchFile)?
    .join("actual_file.txt"))?
// vs.
path
    .parent()
    .ok_or(Error::NoSuchFile)?
    .join("actual_file")
    $(File::open)?

Or for constructing the inner value of some newtype struct Wrapper(Inner)

let value = Wrapper(Inner::new()
    .configure()?
    .some_more());
// vs.
let value = Inner::new()
    .configure()?
    .some_more()
    $(Wrapper);

Or for freestanding functions that can not be member functions because the argument type is from another crate, and a whole trait with import is a lot of overhead.

Of course, you could introduce temporary bindings but sometimes... It's that little bit cleaner. Not with the ad-hoc syntax up there of course.

Edit: Any further discussion on this should be in another thread though

4 Likes

And one other bit of new information raised in this thread I haven’t seen elsewhere: "space await" binding like as rather than like "dot await".

The reason this is interesting is that one of the main arguments against "space await" is that it visually groups wrong when you do future await.context(_)?. If this had to be written (future await).context(_)?, then the flow would always be clear.

(Personally, I see no benefit of “expr space await" over "await space expr” (as now you use parenthesis in all the same places), but this is a new point from this thread that potentially removes a problem of "space await".)

Also, a strong vocal support of doing await expr now and introducing .await later as part of a more general .keyword rather than ad-hocing it for await first. (Though that argument can be made either way esp. in regards to incrementallity.)

2 Likes

As I said, I was merely trying to aggregate arguments that I thought might have originated in this thread, without stating whether I agree or disagree. I must admit I don't really follow the "direction of eye movement" argument, so your comment would probably be better as a reply to @BrianMWest's original comment.

I agree.

With respect to accessibility, if indeed there were evidence that .await made coding with a screen reader more difficult, I'd be inclined to take that seriously regardless of whether ? also makes coding with a screen reader more difficult. Really, I just wish we had someone with specific accessibility needs weighing in on this; as it is, we have many non-disabled programmers (including myself) weighing the pros and cons of the various proposals, apparently with very little knowledge of how to ensure accessibility.

In any case, I do think .await is different from ? in this regard, because ? does not change the "happy path" control flow (which is often[citation needed] considered the primary control flow that should be considered for readability), whereas that is explicitly the purpose of .await. Concretely, if ? changes the control flow, then the remainder of the function (for a specific execution) does not matter.

A specific case in which .await could cause a readability problem that would not be posed by ? would arise when combining async/await code with resource-contentious multithreaded code. A ? following the acquisition of a mutex-lock would terminate the critical section; would await release the lock, or not? Either way, understanding at a glance whether or not a critical section contains an await seems valuable. (I don't actually know how await interacts with Drop types, unfortunately.)

Hm, I suppose that was indeed pretty similar to BrianMWest's point. Sorry for missing the overlap.

3 Likes