-
Notifications
You must be signed in to change notification settings - Fork 13k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolve await
syntax
#57640
Comments
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 task JavaScriptlet 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. |
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. |
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. |
I would like to see that this issue was refered in the top post of #50547 (beside the check box "Final syntax for await."). |
Kotlin's syntax is: val result = doTask() The |
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. |
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? |
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:
|
There's nothing special about So how would that work for Futures which don't return |
It seems it was not clear what I meant. |
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 |
If it's specifically the hint to operator
The best argument why |
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. |
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 |
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 |
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".) |
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:
|
These are different expressions, the dot operator is higher precedence than the "obvious precedence"
This has the exact same ambiguity of whether |
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:
|
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?; |
Actually i think every separator looks fine for postfix syntax, for example: |
Could postfix macro (i.e.
Also postfix macro requires less effort to be implemented, and is easy to understand and use. |
Also it's just using a common lang feature (or at least it would look like a normal postfix macro). |
In C# you write this: try
{
var response = await client.GetAsync("www.google.com");
var json = await response.ReadAsStreamAsync();
var somethingElse = await DoMoreAsyncStuff(json);
}
catch (Exception ex)
{
// handle exception
} Regarding the propagate-error-chaining argument, e.g. let response = await? getProfile(); Another thing that just occurred to me: what if you want to // Prefix
let userId = match await response {
Ok(u) => u.id,
_ => -1
}; // Postfix
let userId = match response {
Ok(u) => u.id,
_ => -1
} await; Additionally, would an let userId = match response {
Ok(u) => somethingAsync(u),
_ => -1
} await; // Are we awaiting match or response here? If you have to await both things, you'd be looking at something like // Prefix - yes, double await is weird and ugly but...
let userId = await match await response {
Ok(u) => somethingAsync(u),
_ => -1
} await; // Postfix - ... this is weirder and uglier
let userId = match response {
Ok(u) => somethingAsync(u),
_ => -1
} await await; Although I guess that might be // Postfix - ... this is weirder and uglier
let userId = match response await {
Ok(u) => somethingAsync(u),
_ => -1
} await; (Programming language design is hard.) |
Regardless, I'm going to reiterate that Rust has precedence for unary operators being prefix with |
Beauty is in the eye of the beholder.
Regardless, I feel that the discussion is now circling the drain. Without bringing up any new discussion points, there is no use to re-iterate the same positions again and again. FWIW, I'm happy to get |
@markrendle I'm not sure what you answering on
I know how I write in C#. I said "imagine how could it look it we didn't have exceptions". Because Rust doesn't.
It was already discussed twice or trice, please, read the topic. In a nutshell: it's an artificial construct, that won't work well if we will have something additional to
// Real postfix
let userId = match response await {
Ok(u) => u.id,
_ => -1
}; // Real Postfix 2 - looks fine, except it's better to be
let userId = match response await {
Ok(u) => somethingAsync(u),
_ => ok(-1)
} await; // Real Postfix 2
let userId = match response await {
Ok(u) => somethingAsync(u) await,
_ => -1
}; |
As another user of C# I'll say that its prefix syntaxes: However, any syntax will be better than chaining futures explicitly, even a pseudo-macro. I will welcome any resolution. |
@orthoxerox You raise a very good point. In my dayjob I mostly write Java and I despise the new operator to a level that all my classes which need explicit instantiation (a surprisingly rare occurence when you use builder patterns and dependency injection) have a static factory method just that I can hide the operator. |
I'm guessing this is probably a language barrier thing because that's not at all what you said, but I'll accept it may have been what you meant. Anyway, as @rolandsteiner said, the important thing is that we get some form of async/await, so I'm happy to await the core team's decision, and all y'all postfix fans can the core team's decision await. 😛 ❤️ ☮️ |
@yasammez Come to C#. In v8.0 we get to just use new() without the type name :) |
I'm just gonna throw out some ideas for the postfix operator. foo()~; // the pause operator
foo()^^; // the road bumps operator
foo()>>>; // the fast forward operator |
Not saying if postfix operator is the way to go or not, but personally I find |
I was proposed I proposed this because it is like an echo talking:
|
|
I can't tell if this thread have hit peak ridiculousness or are we onto something here. |
I think |
Could be:
|
We could introduce a trigraph like |
At first, I was strongly on the side of having a delimiter-required prefix syntax as It makes me think that it is somewhat unfortunate that now there is a confusion in attempting to compare it to other languages with that regard. The closest analog really is the monad I have, therefore, been convinced of the merit of the postfix sigil operator. The downside of this, is that the sigil I would've most preferred is taken by the never type. I would've preferred |
I tend to disagree. The behaviour you mention is not a property of |
Probably the problem with weirdly looking Then providing a better glyph and some ligatures for popular programming fonts (or at least for Mozilla's In all other cases, for me it don't looks that E.g. the following code uses different than // A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
info!("recipient: {}", recipient);
}
// B
match db.load(message.key)@? {
Some(key) => key,
None => {
return Err(/* [...] */);
}
};
// C
let mut res = client.get(&script_src)
.header("cookie", self.cookies.read().as_header_value())
.header("user-agent", USER_AGENT)
.send()@?
.error_for_status()?;
// D
let mut res: InboxResponse = client.get(inbox_url)
.headers(inbox_headers)
.send()@?
.error_for_status()?
.json()@?;
// E
let mut res: Response = client.post(url)
.multipart(form)
.headers(headers.clone())
.send()@?
.error_for_status()?
.json()@?;
// F
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)
} Expand for comparison how it looks with regular ANSI `@`// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
info!("recipient: {}", recipient);
}
// B
match db.load(message.key)@? {
Some(key) => key,
None => {
return Err(/* [...] */);
}
};
// C
let mut res = client.get(&script_src)
.header("cookie", self.cookies.read().as_header_value())
.header("user-agent", USER_AGENT)
.send()@?
.error_for_status()?;
// D
let mut res: InboxResponse = client.get(inbox_url)
.headers(inbox_headers)
.send()@?
.error_for_status()?
.json()@?;
// E
let mut res: Response = client.post(url)
.multipart(form)
.headers(headers.clone())
.send()@?
.error_for_status()?
.json()@?;
// F
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)
} |
Then you probably want unambiguous
It's not true. It's actually is like most other futures from other languages. Difference between "run until first await when spawned" vs "run until first await when polled" is not that big. I know because I worked with both. If you actually think "when this difference comes to play" you will find only corner case. e.g. you will get error when creating future on first poll instead of when it's created. They could be internally different, but they are quite the same from user perspective so it doesn't make sense to me to make a distinction here. |
That's not the difference we're talking about, though. We're not talking about lazy vs eager -to-first-await. What we're talking about is that
Personally, I think this mismatch of semantics is large enough to counteract the familiarity of Anecdotally, for me when I first learned At this point, I believe the discussion on the differences in Rust's |
@CAD97 yes, I see your position, but I think the discinction is not that big. You got me, I got you. So let the discussion have its flow. |
If even people intimately familiar with Rust make that mistake, is it really a mistake? |
So, we had a number of discussions about async-await at the Rust All Hands. In the course of those discussions, a few things became clear: First, there is no consensus (yet) in the lang team about the await syntax. There are clearly a lot of possibilities and strong arguments in favor of all of them. We spent a long time exploring alternatives and produced quite a lot of interesting back-and-forth. An immediate next step for this discussion, I think, is to convert those notes (along with other comments from this thread) into a kind of summary comment that lays out the case for each variant, and then to continue from there. I'm working with @withoutboats and @cramertj on that. Stepping back from the syntax question, another thing that we plan to do is an overall triage of the status of the implementation. There are a number of current limitations (e.g., the implementation requires TLS under the hood presently to thread information about the waker). These may or may not be blockers to stabilization, but regardless they are issues that do need to ultimately be addressed, and that is going to require some concerted effort (in part from the compiler team). Another next step then is to conduct this triage and generate a report. I expect us to conduct this triage next week, and we'll have an update then. In the meantime, I am going to go ahead and lock this thread until we've had a chance to produce the above reports. I feel like the thread has already served its purpose of exploring the possible design space in some depth and further comments aren't going to be particularly helpful as this stage. Once we have the aforementioned reports in hand, we will also lay out the next steps towards reaching a final decision. (To expand a bit on the final paragraph, I am pretty interested in exploring alternative ways to explore the design space beyond long discussion threads. This is a much bigger topic than I can address in this comment, so I won't go into details, but suffice to say for now that I am quite interested in trying to find better ways to resolve this -- and future! -- syntax debates.) |
Async-await status report: http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/ Relative to my previous comment, this contains the results of the triage and some thoughts about syntax (but not yet a full syntax write-up). |
Marking this issue as blocking for async-await stabilization, at least for the time being. |
Quite a while ago, Niko promised that we would write a summary of the discussion in the language team and the community about the final syntax for the await operator. Our apologies for the long wait. A write up of the status of the discussion is linked below. Before that, though, let me also give an update on where the discussion stands now and where we will go from here. Brief summary of where async-await stands right nowFirst, we hope to stabilize async-await in the 1.37 release , which branches on July 4th, 2019. Since we do not want to stabilize the As far as the syntax goes, the plan for resolution is as follows:
The writeupThe writeup is a dropbox paper document, available here. As you'll see, it is fairly long and lays out a lot of the arguments back-and-forth. We would appreciate feedback on it; rather than re-opening this issue (which already has more than 500 comments) I've created an internals thread for that purpose. As I said before, we plan to reach a final decision in the near future. We also feel the discussion has largely reached a stable state: expect the next few weeks to be the "final comment period" for this syntax discussion. After the meeting we'll hopefully have a more detailed timeline to share for how this decision will be made. Async/await syntax is probably the most hotly anticipated feature Rust has gained since 1.0, and the syntax for await in particular has been one of the decisions on which we have received the most feedback. Thank you to everyone who has participated in these discussions over the last few months! This is a choice on which many people have strongly divergent feelings; we want to assure everyone that your feedback is being heard and the final decision will be reached after much thoughtful and careful deliberation. |
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!)
The text was updated successfully, but these errors were encountered: