None of them are easy to change, because of backwards compatibility. Syntax like that can only be changed in a new edition (and even then that's unlikely).
There isn't anything inherently about .await that makes it harder to change. It's the same as any of the other options.
Another point I want to throw against await keyword: it’s very unfriendly for non-English-speaker newcomers in programming.
Let consider frequency rank of Rust keywords on COCA list:
for — 13
if — 40
while — 153
where — 169
continue — 294
else — 443
return — 473
break — 495
match — 1986
yield — 2675
await — 3702
loop — 4294
And on another list based on Google Trillion Word Corpus (IMO, this is more representative):
for — 7
if — 37
where — 159
while — 308
return — 357
else — 833
continue — 1056
break — 1618
match — 1536
yield — 4557
loop — 3536
await — 19022 (!)
It’s clear that in this perspective await is very unfamiliar keyword
Talking from my experience I saw it for the first time in program’s code and it wasn’t immediately clear what it means. The first thought was that it’s “asynchronously wait” abbreviation or something like that, so additionally to pretty complex concept I was required to understand a new uncommon English word, plus its proper meaning. And the same experience reoccurred with yield. Moreover, I’ve learned these words ~4 years ago and until today I don’t remember that I’ve met await or yield somewhere outside of programming context!
Well, that's at least not so much true for the German language.
The literal translation erwarten is the word for
to await and to expect, thus somewhat frequent.
to wait -- warten
to await sth. -- etwas erwarten, auf etwas warten
die lang erwartete Post -- the long awaited mail
sie konnte die Antwort kaum erwarten -- she could hardly wait for the answer
sie konnte die Antwort nicht erwarten -- she couldn't wait for the answer
ich erwarte von dir, dass du -- I expect you to (lit. I await from you, that you)
erwartungsgemäß -- as expected
das kam unerwarted -- that was unexpected (lit. that came)
If you really care about learnability of your concepts then their names not only should be different, but also they should provide a meaningful associations for learners. Otherwise, naming concepts would be nothing than generating a random strings and ensuring their uniqueness.
My whole point was that specific group of people might not have a proper associations to easily grasp what "await" means, and that it should be considered.
How does “wait” (without the leading a-) do in the same distributions, or in translation? Also would changing the leading sigil from . or adding a trailing !, help, in your view/this regard?
"wait" is on 401 and 2071 positions on first and second frequency lists, respectively, however for me it's very familiar word and it's immediately clear what it means. Moreover, on postfix position it feels a bit more declarative and better attached to local context instead of parent scope, e.g.
What I also like is that simple "wait" don't represents a completed action. It's treated more like command which execution is uncertain. The sequence future.wait.expect reads almost naturally.
And another good thing about this syntax is that it don't creates the wrong impression that either something.match and await future should be possible.
Absolutely not, any symbol other than "dot" or "space" just introduces yet another point of confusion
So “wait” is more common and even more clearly a verb. Funny how we’ve ended up here, effectively being asked to settle for and accept field access syntax (leading dot ., all lower case, no () or !) when await (transitive) and wait (imperative) are both clearly verbs.
IMO, for example #wait or #await is clearly better than .awaitbecause it forces, in your words, a “point of confusion” (or in my words, an opportunity to learn the control flow feature) that isn’t visually lost. Of course, the prefix syntax also avoids the issue.
´await´ and ´yield´ are both used in many other languages. I would think that having keywords consistent with other programming languages would make it easier, not harder for foreign language learners in the long run.
Is postfix #yield an option that was, or would be considered, as a direct replacement for .await? I’m presently unclear if the meaning (as found in other languages) is the same?
Edit: Is it safe to say that in the context of an async fn or block, that yield is the generic operation that means wait?
That's a value judgment, and stylistic design choice, isn't it? I mean to implement the current "final proposal" the compiler will take an AST for the async fn or block and transform it into a generator. Generally, rust has favored being very explicit, for example with Clone (vs implicit Copy) Why shouldn't it be an explicit (postfix, reserved word) #yield here? If the only reason is that the language team doesn't want to stabilize async generators, then that seems a weak argument (e.g. under-motivated) for introducing this field access styled .await that we will be stuck with forever for compatibility.
async gen fn lines(file: &Path) -> io::Result<usize> yield &str {
let file = File::open(file).await?;
let reader = AsyncBufReader::new(file);
let mut buf = String::new();
let mut line_count = 0;
while !reader.is_empty() {
line_count += 1;
reader.read_line(&mut buf).await?;
yield return &buf;
}
return Ok(line_count);
}
Does this illustrate the difference between yield and await in the same context? In any case, the same syntax bikeshed applies no matter the keyword, if it does the same operation.
async is implemented as loop and yield, but that’s invisible to the async function, except as observed behavior of Future::poll.
That just means that yield takes a T and returns a U. It is still absolutely not the same thing as await.
No, it isn't. It's much more than that. As shown in the code example I posted, and in the types.
As I explained, yield and await are opposites. They do completely different things. They are used for completely different purposes.
If they were called the same thing, then you would not be able to create an asynchronous generator.
Other languages like C# and JavaScript have both await and yield. That is not weird. The weird thing would be to combine them.
The Rust compiler can implement async however it wants. It does not have to use generators. It can use whatever internal systems it wants.
The Rust compiler can use special generator magic internally, without exposing that generator magic to users. That's an implementation detail, not something that should decide the user-facing name of the operator.
Let's please stop talking about generators, they are completely irrelevant to async / await (unless you're working on the Rust compiler).
Just to add to this, since it had me looking for connections where there need not be one for a while as well. The difference is not only in the implementation, it starts at a conceptual level. While yield seems similar at first (throw something in, get something out), the result of await is determined by the argument you give it while the result of yield is not. So for the former you determine the result of your 'awaitee' (the future), for the latter your caller does (if generators with resumption arguments are to be stabilized as such). I found that notion useful for viewing them as actual opposites.
I believe the symbol can be called “part of”, as in localhost:80 – the 80 is a part of localhost.
(Maybe I shouldn’t say the following, but,)
However, I personally generally don’t like the postfix syntax. await something works the best for me.
let val = await something // await for something
let val = await something? // evaluate `something?` first, then await for the result
let val = (await something)? // await for something, then do the ?
let result1 = (await something)?
let result2 = (await result1)?
I personally also don’t like chaining, because control flow is a tree, not a chain. Chaining can sometime make the code hard to understand, plus you often need to break the chain in order to make the control flow to work.
I mean, chaining can be a good candy to eat, but design a syntax to accommodate it maybe is a little bit over do?
it is important since -- if they are added -- it will invalidate the often-repeated point that yield and await don't need to have the same syntax because await returns something, while yield does not:
If generator resume arguments are added, yield will be an expression.
I'm not saying they should be called the same thing. Sure, it would be nice to get async generators. But I don't see what difference it makes about my point, which is that yield and await are equivalent:
can you implement await using yield? Yes, and that's how it's done today. While it could change, I don't expect it to happen, since they're fundamentally the same transformation.
can you implement yield using await? I haven't tried, but I expect so: you can make a bounded async channel of capacity 1, wrap the reading end in an iterator, and block on it in next. I won't say I understand the subtleties of the pull model of futures in Rust, so it may or may not be enough, but the idea seems sound to me.
So where do we stand?
If generator resume arguments are added, both yield and await will return values. You may still say they're not the same, or not supposed to be used the same way, but that's a different argument, based on what's understood to be the usual idioms, not a fundamental difference.
Even now, you can implement one with the other, which again shows there is no fundamental difference.
Consider the proposed prefix await? operator. Is it syntactic sugar? Yes, and some people don't like it. However, it's the good kind of sugar, since it combines two orthogonal features in a clear way, making them easier to use together in a way that is supposed to be common. It's like for in C: redundant, but useful nonetheless, and not really hard to understand. C code wouldn't be better by any metric if there was no for. Or better, like a contraction -- saying "it's" instead of "it is".
Now compare that to what you're proposing for yield and await. They're two features that work the same way -- they suspend a function until someone else decides to resume it. But you want vastly different syntaxes for them (actually a new kind of syntax for one) because of some perceived expectation that awaitmust be chained while yield must not, instead of being based on what they are.
The caller of a generator can use the values to pick the arguments that are passed back in. It's a common idea in code based off coroutines. Neither the generator, nor the async function can actually control what they get back on resumption. More so, they work the same way:
they suspend the execution of the current function
they give you a value that can be used resume the function (an impl Future, respectively whatever the thing that generators hand out is called)
any part of the code can decide to resume them at any time
Once you go outside of the async IO / iteration mindset, you can see both of these features used in a more general way, similarly to coroutines or say actor systems.