Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upResolve `await` syntax #57640
Comments
Centril
added
A-async-await
C-tracking-issue
T-lang
labels
Jan 15, 2019
This comment has been minimized.
This comment has been minimized.
|
I thought it might be useful to write up how other languages handle an await construct. Kotlinval result = task.await()C#var result = await task;F#let! result = task()Scalaval result = Await.result(task, timeout)Pythonresult = await taskJavaScriptlet result = await task;C++ (Coroutines TR)auto result = co_await task;Hack$result = await task;Dart
With all that, let's remember that Rust expressions can result in several chained methods. Most languages tend to not do that. |
This comment has been minimized.
This comment has been minimized.
I'd say that languages that support extension methods tend to have them. These would include Rust, Kotlin, C# (e.g. method-syntax LINQ and various builders) and F#, although the latter heavily uses the pipe operator for the same effect. |
This comment has been minimized.
This comment has been minimized.
|
Purely anecdotal on my part but I regularly run in to dozen+ method chained expressions in Rust code in the wild and it reads and runs fine. I haven't experienced this elsewhere. |
This comment has been minimized.
This comment has been minimized.
earthengine
commented
Jan 15, 2019
|
I would like to see that this issue was refered in the top post of #50547 (beside the check box "Final syntax for await."). |
This comment has been minimized.
This comment has been minimized.
kvinwang
commented
Jan 16, 2019
•
Kotlin's syntax is: val result = doTask()The |
This comment has been minimized.
This comment has been minimized.
|
Thank you for mentioning that. Kotlin feels more implicit because futures are eager by default. It's still however a common pattern in a deferred block to use that method to wait on other deferred blocks. I've certainly done it several times. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
maybe you should add both use cases with a bit of context/description. Also what's with other langs using implicit awaits, like go-lang? |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Jan 16, 2019
•
|
One reason to be in favour of a post-fix syntax is that from the callers perspective, an await behaves a lot like a function call: You relinquish flow control and when you get it back a result is waiting on your stack. In any case, I'd prefer a syntax that embraces the function-like behaviour by containing function-paranthesis. And there are good reasons to want to split construction of coroutines from their first execution so that this behaviour is consistent between sync and async blocks. But while the implicit coroutine style has been debated, and I'm on the side of explicitness, could calling a coroutine not be explicit enough? This probably works best when the coroutine is not directly used where constructed (or could work with streams). In essence, in contrast to a normal call we expect a coroutine to take longer than necessary in a more relaxed evaluation order. And So, after hopefully having provided a somewhat new take on why post-fix could be preferred, a humble proposal for syntax:
Adapting a fairly popular example from other thread (assuming the
And with the alternative:
To avoid unreadable code and to help parsing, only allow spaces after the question mark, not between it and the open paran. So The interaction with other post-fix operators (such as the current
Or
A few reasons to like this:
And reasons not to like this:
|
This comment has been minimized.
This comment has been minimized.
There's nothing special about So how would that work for Futures which don't return |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Jan 17, 2019
•
|
It seems it was not clear what I meant. |
This comment has been minimized.
This comment has been minimized.
Thanks for clarifying. In that case I'm definitely not a fan of it. That means that calling a function which returns a It's very syntax-heavy, and it uses |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Jan 17, 2019
|
If it's specifically the hint to operator
The best argument why |
This comment has been minimized.
This comment has been minimized.
i thought that's the whole point of explicit awaits?
yeah, so the this syntax is like calling a function that returns a closure then calling that closure in JS. |
This comment has been minimized.
This comment has been minimized.
|
My reading of "syntax-heavy" was closer to "sigil-heavy", seeing a sequence of
I think the rebuttal here is: how many times have you seen |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Jan 17, 2019
I don't see how this choice of syntax has an influence on the number of times you have to explicitely name your result type. I don't think it does not influence type inference any different than However, you all made very good points here and the more examples I contrive with it (I edited the original post to always contain both syntax version), the more I lean towards |
This comment has been minimized.
This comment has been minimized.
I expect to see the type equivalent of this (an async fn that returns a Result) all the time, likely even the majority of all async fns, since if what you're awaiting is an IO even, you'll almost certainly be throwing IO errors upwards. Linking to my previous post on the tracking issue and adding a few more thoughts. I think there's very little chance a syntax that does not include the character string
My own ranking amonst these choices changes every time I examine the issue. As of this moment, I think using the obvious precedence with the sugar seems like the best balance of ergonomics, familiarity, and comprehension. But in the past I've favored either of the two other prefix syntaxes. For the sake of discussion, I'll give these four options these names:
(I've specifically used "postfix keyword" to distinguish this option from other postfix syntaxes like "postfix macro".) |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Jan 17, 2019
•
|
One of the shortcomings of 'blessing' An example where we can recover from an error case:
This issue does not occur for actual post-fix operators:
|
This comment has been minimized.
This comment has been minimized.
These are different expressions, the dot operator is higher precedence than the "obvious precedence"
This has the exact same ambiguity of whether |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Jan 17, 2019
•
Not exactly the same.
That's part of the confusion, replacing
However, under 'Useful precedence' and
whereas in obvious precedence this transformation no longer applies at all without extra paranthesis, but at least intuition still works for 'Result of Future'. And what about the even more interesting case where you await at two different points in a combinator sequence? With any prefix syntax this, I think, requires parantheses. The rest of Rust-language tries to avoid this at lengths to make 'expressions evaluate from left to right' work, one example of this is auto-ref magic. Example to show that this gets worse for longer chains with multiple await/try/combination points.
What I'd like to see avoided, is creating the Rust analogue of C's type parsing where you jump between Table entry in the style of @withoutboats:
|
This comment has been minimized.
This comment has been minimized.
|
I'm strongly in favor of a postfix await for various reasons but I dislike the variant shown by @withoutboats , primarily it seems for the same reasons. Eg. First lets look at a similar table but adding a couple more postfix variants:
Now let's look at a chained future expression:
And now for a real-world example, from let res: MyResponse = client.get("https://my_api").send().await?.json().await?; |
This comment has been minimized.
This comment has been minimized.
|
Actually i think every separator looks fine for postfix syntax, for example: |
This comment has been minimized.
This comment has been minimized.
mzji
commented
Jan 18, 2019
•
|
Could postfix macro (i.e.
Also postfix macro requires less effort to be implemented, and is easy to understand and use. |
This comment has been minimized.
This comment has been minimized.
Also it's just using a common lang feature (or at least it would look like a normal postfix macro). |
This comment has been minimized.
This comment has been minimized.
|
On Tue, Jan 29, 2019 at 11:31:32PM -0800, Sphericon wrote:
I've been following this discussion from afar and I have a few questions and comments.
My first comment is that I believe the `.await!()` postfix macro satisfies all of Centril's main objectives with the exception of the first point:
> "`await` should remain a keyword to enable future language design."
As one of the primary proponents of the `.await!()` syntax, I
*absolutely* think `await` should remain a keyword. Given that we'll
need `.await!()` to be built into the compiler anyway, that seems
trivial.
|
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 30, 2019
•
I don't get this "chaining for chaining" approach. Chaining is good when it's good, e.g. it doesn't make to create useless temp variables. You say "you removed chaining", but you can see that it provides no value here.
No, you did this async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
let user = fetch(format!("/user/{0}", name).as_str()) await?;
let user: User = dbg!(serde_json::from_str(dbg!(user)));
let permissions = fetch(format!("/permissions/{0}", x.id).as_str()) await?;
let permissions: Vec<Permission> = dbg!(serde_json::from_str(dbg!(permissions));
Ok(user)
}It's not the same thing.
It's only a problem for prefix
Why not create these bindings? Bindings are always better for reader, for instance, and if you have a debugger which could print a binding value, but which couldn't evaluate an expression. Finally, you didn't actually removed any bindings. You just used labmda In a nutshell: chaining doesn't provide value by itself. It may be useful in some scenarios, but it's not one of them. And since I write async/await code for more than six years, I do believe you don't want write chained async code ever. Not because you couldn't, but because it's inconvenient and binding-approach is always nicer to read and often to write. Not to say that you don't need combinators at all. If you have |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 30, 2019
|
@Sphericon AFAIK community agreed on using
Can you be more detailed? I didn't see any difference between JS/C# await's, except futures being poll-based, but it have really nothing to deal with |
This comment has been minimized.
This comment has been minimized.
rolandsteiner
commented
Jan 30, 2019
•
|
Regarding the let foo = await? bar_async();How would this look with let foo = await?? double_trouble();IOW, prefix *) edited. Thanks to @Nemo157 for pointing it out. |
This comment has been minimized.
This comment has been minimized.
@rolandsteiner by "futures of futures" do you mean
|
This comment has been minimized.
This comment has been minimized.
rolandsteiner
commented
Jan 30, 2019
|
@Nemo157 You're right of course: Result of Results. Updated my comment. |
This comment has been minimized.
This comment has been minimized.
earthengine
commented
Jan 30, 2019
•
|
Today, we write
And if we write But is it the case that case 1 can always turned into case 2? In case 1, the expression either produce a future, or an error. But the error can be delayed and moved inside the future. So we can just handle case 2 and make an automatic convertsion happening here. In the programmer's point of view, So the proposal is:
For more complicated cases, we can apply something like the automatic method receiver dereference:
I was a postfix keyword/sigil supporter and still am. However I just want to show that the prefix precedence may not be a big issue in pratice and have workarounds. I know Rust team members don't like implicit things, but in such a case, it is just have too little difference between the potential sementics and we have a good way to ensure we do the right thing. |
This comment has been minimized.
This comment has been minimized.
Systemcluster
commented
Jan 30, 2019
Special cases are bad to compose, expand, or learn, and eventually turn into baggage. It's not a good compromise to make exceptions to the rules of the language for a single theoretical usability use-case. |
This comment has been minimized.
This comment has been minimized.
|
Would it be possible to implement Future for |
This comment has been minimized.
This comment has been minimized.
llambda
commented
Jan 30, 2019
•
|
Additional arguments for
Example:
However, after playing around with this in an editor, it feels cumbersome still. I think I would rather have |
This comment has been minimized.
This comment has been minimized.
You would want the inverse. The most common awaitable is a Future where it's output type is a Result. There is then the explicit argument agaisnt hiding or otherwise absorbing the |
This comment has been minimized.
This comment has been minimized.
|
If you have a If you have a There's no hiding or absorbing the |
This comment has been minimized.
This comment has been minimized.
|
Oh. I must have misunderstood you then. I don't see how that helps though as we still need to combine the Oh. The |
This comment has been minimized.
This comment has been minimized.
|
Exactly. So we'd just make the |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 30, 2019
|
@ivandardi what's about |
This comment has been minimized.
This comment has been minimized.
|
@Pzixel Note, that form of future is gone. There is a single associated type now, Output (which probably will be a Result). @ivandardi Okay. I see it now. The only thing you'd have against you there is the precedence being something strange you'd have to learn there as it's a deviation but so is most of anything with Though a Result that returns a future is so rare I haven't found one case apart from something in tokio core that was removed so I don't think we need any sugar/trait impls to help in that case. |
This comment has been minimized.
This comment has been minimized.
Well, I'd assume that that is not possible, seeing that the @mehcode Well, it does address some of the concerns that were brought up previously, I'd say. It also helps decide on the prefix syntax, because there would be just one prefix await syntax instead of "Obvious Precedence" and "Useful Precedence" choices. |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 30, 2019
•
why not? fn probably_get_future(val: u32) -> Result<impl Future<Item=i32, Error=u32>, &'static str> {
match val {
0 => Ok(ok(15)),
1 => Ok(err(100500)),
_ => Err("Coulnd't create a future"),
}
} |
This comment has been minimized.
This comment has been minimized.
|
@Pzixel See https://doc.rust-lang.org/std/future/trait.Future.html You are talking about the old trait that was in the |
This comment has been minimized.
This comment has been minimized.
I60R
commented
Jan 30, 2019
•
|
Honestly, I don't think that having a keyword in prefix position how it was supposed to be: async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = (yield self.request(url, Method::GET, None, true)))?;
let user = (yield user.res.json::<UserResponse>())?;
let user = user.user.into();
Ok(user)
}has any strong advantage over having a sigil in the same position: async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = (*self.request(url, Method::GET, None, true))?;
let user = (*user.res.json::<UserResponse>())?;
let user = user.user.into();
Ok(user)
}I think that it only looks inconsistently with other prefix operators, adds redundant whitespace before expression, and shifts code to the right side at noticeable distance. We can try using sigil with extended dot syntax (Pre-RFC) which resolves issues with deeply nested scopes: async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = self.[*request(url, Method::GET, None, true)]?;
let user = user.res.[*json::<UserResponse>()]?;
let user = user.user.into();
Ok(user)
}as well as adds possibility to chain methods: async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = self.[*request(url, Method::GET, None, true)]?
.res.[*json::<UserResponse>()]?
.user
.into();
Ok(user)
}And obviously, let substitute async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = (@self.request(url, Method::GET, None, true))?;
let user = (@user.res.json::<UserResponse>())?;
let user = user.user.into();
Ok(user)
}async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = self.[@request(url, Method::GET, None, true)]?;
let user = user.res.[@json::<UserResponse>()]?;
let user = user.user.into();
Ok(user)
}async fn request_user(self, user_id: String) -> Result<User> {
let url = format!("users/{}/profile", user_id);
let user = self.[@request(url, Method::GET, None, true)]?
.res.[@json::<UserResponse>()]?
.user
.into();
Ok(user)
}What I like here is that Any thoughts on this? |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 30, 2019
Any extra braces are just a no-go @mehcode Yep, I didn't realize that futures now lack |
This comment has been minimized.
This comment has been minimized.
melodicstream
commented
Jan 30, 2019
Yes! According to Centril's comment, sigils aren't very greppable. I'd only start to consider sigils if one can create a regex that identifies all the await points. As for your extended dot syntax proposal, you'd have to explain it a lot deeper, providing the semantics and desugaring of every usage of it. As of currently, I can't understand the meaning of the code snippets you posted with that syntax.
So you'd have a As in, suppose that await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar()) |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 30, 2019
•
No, I mean With postfix variant you just do let bar = foo()? await?.bar(); |
This comment has been minimized.
This comment has been minimized.
I60R
commented
Jan 30, 2019
|
@melodicstream I updated my post and added link to pre-RFC for that feature |
This comment has been minimized.
This comment has been minimized.
huxi
commented
Jan 30, 2019
|
Oh dear... I just re-read all the comments for the third time... The only consistent feeling is my dislike for the I'd go with the super basic De-sugaring With that being said, I'm quite happy that I'm not in a position to decide about this. I'll now leave this issue alone instead of adding further noise and await (pun intended) the final verdict. ... at least I'll honestly try to do that... |
This comment has been minimized.
This comment has been minimized.
melodicstream
commented
Jan 30, 2019
|
@Pzixel Assuming await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap(); |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
Jan 31, 2019
•
|
@melodicstream no, it won't. You cannot await In postfix way it will be So your examples just don't work, especially the last one, because it should be (await foo.unwrap()).unwrap()However, @huxi may be right we are solving the problem that probably doesn't exist. The best way to figure it out it allow postfix macro and see real codebase after basic |
This comment has been minimized.
This comment has been minimized.
melodicstream
commented
Jan 31, 2019
|
@Pzixel That's why I made the proposal to implement |
This comment has been minimized.
This comment has been minimized.
earthengine
commented
Jan 31, 2019
•
|
Although I am OK with (await foo.unwrap()).unwrap()Otherwise I will be really confusing. The reason is that The postfix syntax, |
This comment has been minimized.
This comment has been minimized.
Swoorup
commented
Jan 31, 2019
|
postfix style macro solves much of these problems nicely, but just the question of do we want to retain familiarity with existing languages and keep it prefixed. I'd vote for postfix style. |
cramertj commentedJan 15, 2019
•
edited by scottmcm
Before commenting in this thread, please check #50547 and try to check that you're not duplicating arguments that have already been made there.
Notes from shepherds:
If you're new to this thread, consider starting from #57640 (comment), which was followed by three great summary comments, the latest of which were #57640 (comment). (Thanks, @traviscross!)