A final proposal for await syntax

I agree

Unless you have all-caps fields/methods.

2 Likes

Thanks for that. Would it still be a breaking change if .ALLCAPS (with proceeding DOT) was special cased only, or even if just .AWAIT is special cased only in the 2018 edition and in async fns/blocks? Perhaps I’m forgetting of some current use case, but I don’t think there is any current use of that, is there? I believe its written via Type::ALLCAPS for constants including associated constants.

If however, none of that works, and possibly in any case, my recommendation remains to change the sigil to one of: # @ ¡.

Note: I’ve taken some heat for the postbang ¡ sigil suggestion, and while I mostly accept the accessibility arguments against it, I leave it on the list because of various other advantages, including: parity weirdness with this very weird language feature—“postfix unary keyword/named operators”.

¡Keep Rust weird!™

1 Like

I can't think of a case where all-cap fields or methods wouldn't be considered exceptional. Its even linted against. Thus a syntax collision with it would not be a failure of rust language/syntax design. We'd live with that, I think, couldn't we?

1 Like

Hey, you guys know that you’re trying to archive three things at same time, right?

  1. Introducing await
  2. Making that await syntax to be more friendly for ?
  3. Making that await syntax to be more friendly to chaining.

It’s kind of over greedy, you know. Maybe things will be easier when given up on either point 2 or 3?

I mean, if you only want to have 1 and 2, you can have:

let result = (await task)?
let result2 = (await result)?

or maybe

let result = task await?
let result2 = result await?

If you want to have 1 and 3, you can have

let result = task.await.another_operation().await.yet_another_operation()

Both of them looks nice and clean.

But with all 1, 2, 3:

let result = task.await?.another_operation()?.yet_another_operation()

It just look a bit weird anyhow.

This certainly looks clean, but it shouldn't be that clean. This particular code used 2 awaits, which might slow down the program significantly. I want await keyword to be noticeable from the orbit.

2 Likes

Here are the design proofs of these latest alternatives. If anyone remains interested or is undecided, I would strongly encourage you to clone this repo and play with the source in your favorite editor, including modifying highlighting for the .AWAIT as a keyword.

future.AWAIT

I remain of the opinion that upcase .AWAIT fixes both the recognition and field access confusion problems, it doesn’t require unusual-whitespace/comments or changing the sigil. To continue with the design-is-analog intuition, we could say this is just a very slight move to the right vs the status quo.

/*magic «*/.await/*»*/ comments workaround

And just for completeness, my best workaround for the status quo. I sincerely hope it doesn’t come to this.

Thanks, as always, for your patience and consideration.

2 Likes

This is a breaking change, because AWAIT isn't a keyword, so struct Foo { AWAIT: u32 } is legal (though non-idiomatic) code.

5 Likes

As I had asked @gbutler above: if it was only applied (as a special keyword and postfix unary operator) in an async block or fn (which do not exist in stable, yet) then it shouldn’t be able to break anything. Or is my logic wrong?

Or is it just that doing that would be particularly horrible from an implementation perspective in the compiler?

As usual, macros make everything awkward. What does foo!(x.AWAIT) mean if foo is a macro that takes an expr?

There's probably a way to make it work, but things like that are why I prefer it just being a keyword: that way it works the same everywhere, and it makes it easier to give targeted errors about its misuse.

9 Likes

I won’t suggest the .AWAIT alternative isn’t a complication, and I apologize for that, but I think its a meaningful and worthy alternative. Some strategies for easing the change in with no, or very minimal breakage:

  • AWAIT outside of async blocks/fn’s, gets a warning level lint, saying that it will become a keyword soon (next edition or 3 releases from now)
  • .AWAIT is in scope, if you will, only in async blocks/fn’s.
  • Its new meaning as postfix unary operator, applies after macro expansion in async blocks/fn’s. (Or am I not understanding the problem you raise?)

Actually, I’m really way out of my depth at this point and should let you and other compiler developers decide if its reasonable to implement. :relaxed:

1 Like

What if you have a field AWAIT that you want to access in a async context. (I know this is contrived, but thats what backwards compatibility means)

1 Like

In that case you are forced to use x.r#AWAIT as part of re-writing to use the async block/fn for the first time, or on nightly, when the change goes into effect. That makes it sort-of breaking, in some abstract sense, but keep in mind that no such stable code exists today, because there are no stable async blocks/fns, correct?

The "next edition" path is what we did to make await a keyword :slightly_smiling_face:

Don't worry about commenting; I'm not on the compiler team either!

I do bet they could figure out a way to make it work; after all you can pass struct to a macro that wants an ident and use it as a keyword, so maybe there's not actually an implementation problem. I just like avoiding contextual keywords in expressions because it avoids strange edge cases that need to get defined.

2 Likes

may_23_meeting.await

5 Likes

Interestingly, that I’ve found the following syntax more visually balanced:

let n = save_file(field).await {}?;
expand for a more complex example

with scope

2019-05-23-081033

and without

2019-05-23-081056

However, I don’t see any use case for this introduced scope. Maybe someone has any suggestion?

Why is it so important to highlight .await over everything else?

There a lot of things that may slow down the program (mutexes, blocking I/O, allocating a lot of memory, etc). All of this can be hidden in inner functions, and we don't have a special syntax for it.

In some languages (like Go) the await operation is implicit, and people have no problems with that.

19 Likes

Let's see:

  • You must call .lock().unwrap() and other functions/methods to use a mutex, it is very noisy, it isn't implicit.
  • I/O operations are done via functions and macros, correct?
  • And you must allocate memory either via declaring a struct or via ::new(), I don't think these are unnoticeable.

You think that .await is a non-special field access?

Go isn't Rust, its goal is different. While both aim to provide safe concurrency, only Rust aims to achieve zero-cost abstraction and try to compete with the likes of C and C++.

If that isn't enough to convince you, C++ clones object by default while Rust requires explicit .clone().

2 Likes

You skipped the All of this can be hidden in inner functions in all quotes, which is the important key of my point.

In a code like:

let data = input.get_data();

We don’t know what is happening in get_data. We have to check either its documentation or its implementation. And, IMO, this is a reasonable trade-off. We can’t expect to be explicit about everything all the time and everywhere.

8 Likes

The reason why it is important in asynchronous functions that the points where await is used are noticable is because it is the caller’s duty to determine when to await. When calling a method that takes some time to compute, you don’t have a choice in whether you wait it out or not, because you have to.

In the experience report from a few days ago, the situation surfaced where the futurres were awaited in the wrong order because typing .await unconsciously is seemingly too easy and does not lead to putting enough thought into where to use it. Asynchronicity is best applied to tasks that can be worked on in parallel, but in order to do so, one needs to consider which tasks can be parallelized and write their code in accordance. Thus, a more noisy approach than a simple .await may have some merit.

I am not going to voice my preference on either of the many suggestions so far, but I feel like this point has been overlooked since we’ve heard the first experience report.

10 Likes

input.get_data() (method call) tells me that "the program is going to perform an action, and that action may be expensive", input.data (field access) on the other hand tell me that "hey, this costs you nothing, do it as much as you want". As a result, I tend to assign input.get_data() to a data variable but leave input.data as is.

example

This is how I handle input.get_data():

let data = input.get_data(); // this is a method call, which might be expensive, therefore I must assign it to a variable if I want to use it multiple times
useItOnce(&data);
useItTwice(&data);

On the other hand, this is how I handle input.data:

useItOnce(&input.data);
useItTwice(&input.data);

Unfortunately, future.await looks like a field access, which tells me "I cost you nothing, use me as much as you want". It lied.

So to answer you: Although we do not know what is happening in .get_data(), we know for sure that it is not zero-cost, therefore we tend to invoke such calls as little as possible. Field access on the other hand is implied to be zero-cost, so for readability’s sake, we usually don't assign them to a variable.

1 Like