Closed cramertj closed 5 years ago
I thought it might be useful to write up how other languages handle an await construct.
val result = task.await()
var result = await task;
let! result = task()
val result = Await.result(task, timeout)
result = await task
let result = await task;
auto result = co_await task;
$result = await task;
var result = await task;
With all that, let's remember that Rust expressions can result in several chained methods. Most languages tend to not do that.
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
val result = task.await()
Kotlin's syntax is:
val result = doTask()
The await
is just a suspendable function
, not a first-class thing.
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.
@cramertj Since there are 276 comments in https://github.com/rust-lang/rust/issues/50547, could you summarize the arguments made there to make it easier to not repeat them here? (Maybe add them to the OP here?)
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 .await!()
is more or less an attempt to differentiate betwen normal calls and coroutine calls.
So, after hopefully having provided a somewhat new take on why post-fix could be preferred, a humble proposal for syntax:
future(?)
future(await)
which comes with its own tradeoffs of course but seems to be accepted as less confusing, see bottom of post.Adapting a fairly popular example from other thread (assuming the logger.log
to also be a coroutine, to show what immediately calling looks like):
async fn log_service(&self) -> T {
let service = self.myService.foo(); // Only construction
self.logger.log("beginning service call")(?);
let output = service(?); // Actually wait for its result
self.logger.log("foo executed with result {}.", output)(?);
output
}
And with the alternative:
async fn log_service(&self) -> T {
let service = self.myService.foo(); // Only construction
self.logger.log("beginning service call")(await);
let output = service(await);
self.logger.log("foo executed with result {}.", output)(await);
output
}
To avoid unreadable code and to help parsing, only allow spaces after the question mark, not between it and the open paran. So future(? )
is good while future( ?)
would not be. This issues does not arise in the case of future(await)
where all current token can be used as previously.
The interaction with other post-fix operators (such as the current ?
-try) is also just like in function calls:
async fn try_log(message: String) -> Result<usize, Error> {
let logger = acquire_lock()(?);
// Very terse, construct the future, wait on it, branch on its result.
let length = logger.log_into(message)(?)?;
logger.timestamp()(?);
Ok(length)
}
Or
async fn try_log(message: String) -> Result<usize, Error> {
let logger = acquire_lock()(await);
// Very terse, construct the future, wait on it, branch on its result.
let length = logger.log_into(message)(await)?;
logger.timestamp()(await);
Ok(length)
}
A few reasons to like this:
.await!()
it does not allude to a member that could have other uses. ?
. This keeps the number of precendence classes lower and helps with learning. And function calls have always been somewhat special in the language (even though the have a trait), so that there is no expectation of user code being able to define their own my_await!()
that has very similar syntax and effect.FnOnce
while Streams
would behave like a FnMut
. Additional arguments may also be accomodated easily.Futures
before, this captures how a ?
with Poll
should have worked all along (unfortunate stabilization sequence here). As a learning step, it is also consistent with expecting a ?
based operator to divert control flow. (await)
on the other hand would not satisfy this but afterall the function will always expect to resume at the divergent point.And reasons not to like this:
?
appears to be an argument but it is not even applied to an expression. I believe this could be solved through teaching, as the token it appears to be applied to is the function call itself, which is the somewhat correct notion. This also positively means that the syntax is unambiguous, I hope.?
can difficult to parse. Especially when you have one future returning a result of another future: construct_future()(?)?(?)?
. But you could make the same argument for being able to a result of an fn
object, leading to expression such as this being allowed: foobar()?()?()?
. Since nevertheless I've never seen this used nor complaint, splitting into separate statements in such cases seems to be required rarely enough. This issues also does not exist for construct_future()(await)?(await)?
-future(?)
is my best shot at a a terse and still somewhat concise syntax. Yet, its reasoning is grounded on implementation details in coroutines (temporarily returning and dispatching on resume), which might make it unsuitable for an abstraction. future(await)
would be an alternative that could still be explainable after await
has been internalized as a keyword but the argument position is a bit hard to swallow for me. It could be fine, and it is certainly more readable when the coroutine returns a result.
future(?)
There's nothing special about Result
: Futures can return any Rust type. It just so happens that some Futures return Result
So how would that work for Futures which don't return Result
?
It seems it was not clear what I meant. future(?)
is what was previously discussed as future.await!()
or similar. Branching on a future that returns a result as well would be future(?)?
(two different ways how we can relinquish control flow early). This makes future-polling (?)
and result testing?
orthogonal. Edit: added an extra example for this.
Branching on a future that returns a result as well would be
future(?)?
Thanks for clarifying. In that case I'm definitely not a fan of it.
That means that calling a function which returns a Future<Output = Result<_, _>>
would be written like foo()(?)?
It's very syntax-heavy, and it uses ?
for two completely different purposes.
If it's specifically the hint to operator ?
which is heavy, one could of course replace it with the newly reserved keyword. I had only initially considered that this feel too much like an actual argument of puzzling type but the tradeoff could work in terms of helping mentally parse the statement. So the same statement for impl Future<Output = Result<_,_>>
would become:
foo()(await)?
The best argument why ?
is appropriate is that the internal mechanism used is somewhat similar (otherwise we couldn't use Poll
in current libraries) but this may miss the point of being a good abstraction.
It's very syntax-heavy
i thought that's the whole point of explicit awaits?
it uses ? for two completely different purposes.
yeah, so the foo()(await)
-syntax would be a lot nicer.
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 ()(?)?
is quite jarring. This was brought up in the original post:
More (and different) mix of paranthesis and
?
can difficult to parse. Especially when you have one future returning a result of another future:construct_future()(?)?(?)?
But you could make the same argument for being able to a result of an
fn
object, leading to expression such as this being allowed:foobar()?()?()?
. Since nevertheless I've never seen this used nor complaint, splitting into separate statements in such cases seems to be required rarely enough.
I think the rebuttal here is: how many times have you seen -> impl Fn
in the wild (let alone -> Result<impl Fn() -> Result<impl Fn() -> Result<_, _>, _>, _>
)? How many times do you expect to see -> impl Future<Output = Result<_, _>>
in an async codebase? Having to name a rare impl Fn
return value to make code easier to read is very different to having to name a significant fraction of temporary impl Future
return values.
Having to name a rare impl Fn return value to make code easier to read is very different to having to name a significant fraction of temporary impl Future return values.
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 await? future
.
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 future(await)
myself. It is not unreasonable to type, and still retains all of the clarity of function-call syntax that this was intended to evoke.
How many times do you expect to see -> impl Future<Output = Result<, >> in an async codebase?
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 await
will be accepted for this syntax. I think at this point, after a year of work on this feature, it would be more productive to try to weigh the pros and cons of the known viable alternatives to try to find which is best than to propose new syntaxes. The syntaxes I think are viable, given my previous posts:
await(future)
or await { future }
. This completely solves the precedence problems, but is syntactically noisy and both delimiter options present possible sources of confusion.?
. (That is, that await binds tighter than ?). This may surprise some users reading code, but I believe functions that return futures of results will be overwhelmingly more common than functions that return results of futures.?
. (That is, that ? binds tighter than await). Additional syntax sugar await?
for a combined await and ? operator. I think this syntax sugar is necessary to make this precedence order viable at all, otherwise everyone will be writing (await future)?
all the time, which is a worse variant of the first option I enumerated.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:
Name | Future | Future of Result | Result of Future |
---|---|---|---|
Mandatory delimiters | await(future) or await { future } |
await(future)? or await { future }? |
await(future?) or await { future? } |
Useful precedence | await future |
await future? |
await (future?) |
Obvious precedence w/ sugar | await future |
await? future or (await future)? |
await future? |
Postfix keyword | future await |
future await? |
future? await |
(I've specifically used "postfix keyword" to distinguish this option from other postfix syntaxes like "postfix macro".)
One of the shortcomings of 'blessing' await future?
in Useful precedence but also others that don't work as post-fix would be that usual patterns of manually converting expressions with ?
may no longer apply, or require that Future
explicitely replicates the Result
-methods in a compatible way. I find this surprising. If they are replicated, it suddenly becomes as confusing which of the combinators work on a returned future and which are eager. In other words, it would be as hard to decide what a combinators actually does as in the case of implicit await. (Edit: actually, see two comments below where I have a more technical perspective what I mean with surprising replacement of ?
)
An example where we can recover from an error case:
async fn previously() -> Result<_, lib::Error> {
let _ = await get_result()?;
}
async fn with_recovery() -> Result<_, lib::Error> {
// Does `or_recover` return a future or not? Suddenly very important but not visible.
let _ = await get_result().unwrap_or_else(or_recover);
// If `or_recover` is sync, this should still work as a pattern of replacing `?` imho.
// But we also want `or_recover` returning a future to work, as a combinator for futures?
// Resolving sync like this just feel like wrong precedence in a number of ways
// Also, conflicts with `Result of future` depending on choice.
let _ = await get_result()?.unwrap_or_else(or_recover);
}
This issue does not occur for actual post-fix operators:
async fn with_recovery() -> Result<_, lib::Error> {
// Also possible in 'space' delimited post-fix await route, but slightly less clear
let _ = get_result()(await)
// Ah, this is sync
.unwrap_or_else(or_recover);
// This would be future combinator.
// let _ = get_result().unwrap_or_else(or_recover)(await);
}
// Obvious precedence syntax
let _ = await get_result().unwrap_or_else(or_recover);
// Post-fix function argument-like syntax
let _ = get_result()(await).unwrap_or_else(or_recover);
These are different expressions, the dot operator is higher precedence than the "obvious precedence" await
operator, so the equivalent is:
let _ = get_result().unwrap_or_else(or_recover)(await);
This has the exact same ambiguity of whether or_recover
is async or not. (Which I argue does not matter, you know the expression as a whole is async, and you can look at the definition of or_recover
if for some reason you need to know whether that specific part is async).
This has the exact same ambiguity of whether or_recover is async or not.
Not exactly the same. unwrap_or_else
must produce a coroutine because it is awaited, so the ambiguitiy is whether get_result
is a coroutine (so a combinator is built) or a Result<impl Future, _>
(and Ok
already contains a coroutine, and Err
builds one). Both of those don't have the same concerns of being able to at-a-glance identify efficiency gain through moving an await
sequence point to a join
, which is one of the major concerns of implicit await. The reason is that in any case, this intermediate computation must be sync and must have been applied to the type before await and must have resulted in the coroutine awaited. There is one another, larger concern here:
These are different expressions, the dot operator is higher precedence than the "obvious precedence" await operator, so the equivalent is
That's part of the confusion, replacing ?
with a recovery operation changed the position of await
fundamentally. In the context of ?
syntax, given a partial expression expr
of type T
, I expect the following semantics from a transformation (assuming T::unwrap_or_else
to exist):
expr?
-> expr.unwrap_or_else(or_recover)
<T as Try>::into_result(expr)?
-> T::unwrap_or_else(expr, or_recover)
However, under 'Useful precedence' and await expr?
(await expr
yields T
) we instead get
await expr?
-> await expr.unwrap_or_else(or_recover)
<T as Try>::into-result(await expr)
-> await Future::unwrap_or_else(expr, or_recover)
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.
// Chain such that we
// 1. Create a future computing some partial result
// 2. wait for a result
// 3. then recover to a new future in case of error,
// 4. then try its awaited result.
async fn await_chain() -> Result<usize, Error> {
// Mandatory delimiters
let _ = await(await(partial_computation()).unwrap_or_else(or_recover))?
// Useful precedence requires paranthesis nesting afterall
let _ = await { await partial_computation() }.unwrap_or_else(or_recover)?;
// Obivious precendence may do slightly better, but I think confusing left-right-jumps after all.
let _ = await? (await partial_computation()).unwrap_or_else(or_recover);
// Post-fix
let _ = partial_computation()(await).unwrap_or_else(or_recover)(await)?;
}
What I'd like to see avoided, is creating the Rust analogue of C's type parsing where you jump between left and right side of expression for 'pointer' and 'array' combinators.
Table entry in the style of @withoutboats:
Name | Future | Future of Result | Result of Future |
---|---|---|---|
Mandatory delimiters | await(future) |
await(future)? |
await(future?) |
Useful precedence | await future |
await future? |
await (future?) |
Obvious precedence | await future |
await? future |
await future? |
Postfix Call | future(await) |
future(await)? |
future?(await) |
Name | Chained |
---|---|
Mandatory delimiters | await(await(foo())?.bar())? |
Useful precedence | await(await foo()?).bar()? |
Obvious precedence | await? (await? foo()).bar() |
Postfix Call | foo()(await)?.bar()(await) |
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. foo await.method()
is confusing.
First lets look at a similar table but adding a couple more postfix variants:
Name | Future | Future of Result | Result of Future |
---|---|---|---|
Mandatory delimiters | await { future } |
await { future }? |
await { future? } |
Useful precedence | await future |
await future? |
await (future?) |
Obvious precedence | await future |
await? future |
await future? |
Postfix keyword | future await |
future await? |
future? await |
Postfix field | future.await |
future.await? |
future?.await |
Postfix method | future.await() |
future.await()? |
future?.await() |
Now let's look at a chained future expression:
Name | Chained Futures of Results |
---|---|
Mandatory delimiters | await { await { foo() }?.bar() }? |
Useful precedence | await (await foo()?).bar()? |
Obvious precedence | await? (await? foo()).bar() |
Postfix keyword | foo() await?.bar() await? |
Postfix field | foo().await?.bar().await? |
Postfix method | foo().await()?.bar().await()? |
And now for a real-world example, from reqwests
, of where you might want to await a chained future of results (using my preferred await form).
let res: MyResponse = client.get("https://my_api").send().await?.json().await?;
Actually i think every separator looks fine for postfix syntax, for example:
let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?;
But i don't have a strong opinion about which one to use.
Could postfix macro (i.e. future.await!()
) still be an option? It's clear, concise, and unambiguous:
Future | Future of Result | Result of Future |
---|---|---|
future.await!() | future.await!()? | future?.await!() |
Also postfix macro requires less effort to be implemented, and is easy to understand and use.
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).
A postfix macro would be nice as it combines the succinctness and chainability of postfix with the non-magical properties and obvious presence of macros, and would fit in well with third-party user macros, such as some .await_debug!()
, .await_log!(WARN)
or .await_trace!()
A postfix macro would be nice as it combines [...] the non-magical properties [...] of macros
@novacrazy the problem with this argument is that any await!
macro would be magic, it's performing an operation that is not possible in user-written code (currently the underlying generator based implementation is somewhat exposed, but my understanding is that before stabilization this will be completely hidden (and actually interacting with it at the moment requires using some rustc
-internal nightly features anyway)).
@Nemo157 Hmm. I wasn't aware it was intended to be so opaque.
Is it too late to reconsider using a procedural macro like #[async]
to do the transformation from "async" function to generator function, rather than a magical keyword? It's three extra characters to type, and could be marked in the docs the same way #[must_use]
or #[repr(C)]
are.
I'm really disliking the idea of hiding so many abstraction layers that directly control the flow of execution. It feels antithetical to what Rust is. User's should be able to fully trace through the code and figure out how everything works and where execution goes. They should be encouraged to hack at things and cheat the systems, and if they use safe Rust it should be safe. This isn't improving anything if we lose low-level control, and I may as well stick to raw futures.
I firmly believe Rust, the language (not std
/core
), should provide abstractions and syntax only if they are impossible (or highly impractical) to do by users or std
. This entire async thing has gotten out of hand in that regard. Do we really need anything more than the pin API and generators in rustc
?
@novacrazy I generally agree with the sentiment but not with the conclusion.
should provide abstractions and syntax only if they are impossible (or highly impractical) to do by users or std.
What is the reason for having for
-loops in the language when they could also be a macro that turns into a loop
with breaks. What is the reason for || closure
when it could be a dedicated trait and object constructors. Why did we introduce ?
when we already had try!()
. The reason why I disagree with those questions and your conclusions, is consistency. The point of these abstractions is not only the behaviour they encapsulate but also the accessibility thereof. for
-replacement breaks down in mutability, primary code path, and readability. ||
-replacement breaks down in verbosity of declaration–similar to Futures
currently. try!()
breaks down in expected order of expressions and composability.
Consider that async
is not only the decorator on a function, but that there other thoughts of providing additional patterns by aync-blocks
and async ||
. Since it applies to different language items, usability of a macro seems suboptimal. Not to even think of implementation if it has to be user visible then.
User's should be able to fully trace through the code and figure out how everything works and where execution goes. They should be encouraged to hack at things and cheat the systems, and if they use safe Rust it should be safe.
I don't think this argument applies because implementing coroutines using entirely std
api would likely heavily rely on unsafe
. And then there is the reverse argument because while it is doable–and you won't be stopped even if there is syntactic and semantic way in the language to do it–any change is going to be heavily at risk of breaking assumptions made in unsafe
-code. I argue that Rust shouldn't make it look like its trying to offer a standard interface to the implementation of bits it doesn't intend to stabilize soon, including the internals of Coroutines. An analogue to this would be extern "rust-call"
which serves as the current magic to make it clear that function calls don't have any such guarantee. We might want to never actually have to return
, even though the fate of stackful coroutines is yet to be decided on. We might want to hook an optimization deeper into the compiler.
Aside: Speaking of which, in theory, not as completely serious idea, could coroutine await be denoted as a hypothetical extern "await-call" fn () -> T
? If so, this would allow in the prelude a
trait std::ops::Co<T> {
extern "rust-await" fn await(self) -> T;
}
impl<T> Co<T> for Future<Output=T> { }
aka. future.await()
in a user-space documented items. Or for that matter, other operator syntax could be possible as well.
@HeroicKatora
Why did we introduce
?
when we already hadtry!()
To be fair, I was against this as well, although it has grown on me. It would be more acceptable if Try
was ever stabilized, but that's another topic.
The issue with the examples of "sugar" you give is that they are very, very thin sugar. Even impl MyStruct
is more or less sugar for impl <anonymous trait> for MyStruct
. These are quality of life sugars that add zero overhead whatsoever.
In contrast, generators and async functions add not-entirely-insignificant overhead and significant mental overhead. Generators specifically are very hard to implement as a user, and could be more effectively and easily used as part of the language itself, while async could be implemented on top of that relatively easily.
The point about async blocks or closures is interesting though, and I concede that a keyword would be more useful there, but I still oppose the inability to access lower-level items if necessary.
Ideally, it would be wonderful to support the async
keyword and an #[async]
attribute/procedural macro, with the former allowing low-level access to the generated (no pun intended) generator. Meanwhile yield
should be disallowed in blocks or functions using async
as a keyword. I'm sure they could even share implementation code.
As for await
, if both of the above are possible, we could do something similar, and limit the keyword await
to async
keyword functions/blocks, and use some kind of await!()
macro in #[async]
functions.
Pseudocode:
// imaginary generator syntax stolen from JavaScript
fn* my_generator() -> T {
yield some_value;
// explicit return statements are only included to
// make it clear the generator/async functions are finished.
return another_value;
}
// `await` keyword would not be allowed here, but the `yield` keyword is
#[async]
fn* my_async_generator() -> Result<T, E> {
let item = some_op().await!()?; // uses the `.await!()` macro
// which would really just use `yield` internally, but with the pinning API
yield future::ok(item.clone());
return Ok(item);
}
// `yield` would not be allowed here, but the `await` keyword is.
async fn regular_async() -> Result<T, E> {
let some_op = async || { /*...*/ };
let item = some_op() await?;
return Ok(item);
}
Best of both worlds.
This feels like a more natural progression of complexity to present to the user, and can be used more effectively for more applications.
Please remember that this issue is specifically for discussion of the syntax of await
. Other conversations about how async
functions and blocks are implemented is out-of-scope, except for the purposes of reminding folks that await!
is not something you can or will ever be able to write in Rust's surface language.
I'd like to specifically weight the pros and cons of all post-fix syntax proposals. If one of the syntaxes stands out with a minor amount of cons, maybe we should go for it. If none however, it would be best to support syntax prefix delimited syntax that is forward compatbile to a yet-to-be determined post-fix if the need arises. As Postfix appears to resonate as being most concise for a few members, it seems practial to strongly evaluate these first before moving to others.
Comparison will be syntax
, example
(the reqwest from @mehcode looks like a real-world benchmark usable in this regard), then a table of (concerns
, and optional resolution
, e.g. if agreed that it could come down to teaching). Feel free to add syntax and/or concerns, I'll edit them into this collective post. As I understand, any syntax that does not involve await
will very likely feel alien to newcomers and experienced users alike but all currently listed ones include it.
Example in one prefix syntax for reference only, don't bikeshed this part please:
let sent = (await client.get("https://my_api").send())?;
let res: MyResponse = (await sent.json())?;
foo() await?
client.get("https://my_api").send() await?.json() await?
Concern | Resolution |
---|---|
Chaining without ? may be confusing or disallowed |
foo().await?
client.get("https://my_api").send().await?.json().await?
Concern | Resolution |
---|---|
Looks like a field |
foo().await()?
client.get("https://my_api").send().await()?.json().await()?
Concern | Resolution |
---|---|
Looks like a method or trait | It may be documented as ops:: trait? |
Not a function call |
foo()(await)?
client.get("https://my_api").send()(await)?.json()(await)?
Concern | Resolution |
---|---|
Can be confused with actual argument | keyword+highlighting+not overlapping |
foo().await!()?
client.get("https://my_api").send().await!()?.json().await!()?
Concern | Resolution |
---|---|
Will not actually be a macro … | |
… Or, await no longer a keyword |
An additional thought on post-fix vs. prefix from the view of possibly incorporating generators: considering values, yield
and await
occupy two opposing kinds of statements. The former gives a value from your function to the outside, the latter accepts a value.
Sidenote: Well, Python has interactive generators where
yield
can return a value. Symmetrically, calls to such a generator or a stream need additional arguments in a strongly type setting. Let's not try to generalize too far, and we'll see that the argument likely transfer in either case.
Then, I argue that it is unatural that these statements should be made to look alike. Here, similar to assignment expressions, maybe we should deviate from a norm set by other languages when that norm is less consistent and less concise for Rust. As expressed otherwhere, as long as we include await
and similarity to other expressions with the same argument order exist, there should be no major hinderance for transitioning even from another model.
Since implicit seems off the table.
From using async/await in other languages and looking at the options here, I've never found it syntactically pleasant to chain futures.
Is a non-chain-able variant on the table?
// TODO: Better variable names.
await response = client.get("https://my_api").send();
await response = response?.json();
await response = response?;
I sort of like this as you could make the argument that it's part of the pattern.
The issue with making await a binding is the error story is less than nice.
// Error comes _after_ future is awaited
let await res = client.get("http://my_api").send()?;
// Ok
let await res = client.get("http://my_api").send();
let res = res?;
We need to keep in mind that nearly all futures available in the community to await are falliable and must be combined with ?
.
If we really need the syntax sugar:
await? response = client.get("https://my_api").send();
await? response = response.json();
Both await
and await?
would need to be added as keywords or we extend this to let
as well, i.e. let? result = 1.divide(0);
Considering how often chaining is used in Rust code, I do entirely agree that it's important for chained awaits to be as clear as possible to the reader. In the case of the postfix variant of await:
client.get("https://my_api").send().await()?.json().await()?;
This generally behaves similar to how I expect Rust code to behave. I do have a problem with the fact that await()
in this context feels just like a function call, but has magical (non function like) behavior in the context of the expression.
The postfix macro version would make this clearer. People are used to exclamation marks in rust meaning "there be magic here" and I certainly have a preference for this version for that reason.
client.get("https://my_api").send().await!()?.json().await!()?;
With that said, it's worth considering that we do already have try!(expr)
in the language and it was our precursor for ?
. Adding an await!(expr)
macro now would be entirely consistent with how try!(expr)
and ?
were introduced to the language.
With the await!(expr)
version of await, we have the option of either migrating to a postfix macro later, or adding a new ?
styled operator such that chaining becomes easy. An example similar to ?
but for await:
// Not proposing this syntax at the moment. Just an example.
let a = perform()^;
client.get("https://my_api").send()^?.json()^?;
I think we should use await!(expr)
or await!{expr}
for now as it's both very reasonable and pragmatic. We can then plan on migrating to a postfix version of await (ie .await!
or .await!()
) later on if/once postfix macros become a thing. (Or eventually going the route of adding an additional ?
style operator... after much bikeshedding on the subject :P)
FYI, Scala's syntax is not Await.result
as that is a blocking call. Scala's Futures are monads, and therefore use normal method calls or the for
monad comprehension:
for {
result <- future.map(further_computation)
a = result * 2
_ <- future_fn2(result)
} yield 123
As a result of this horrid notation, a library called scala-async
was created with the syntax which I am most in favor of, which is as follows:
import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}
val future = async {
val f1 = async { ...; true }
val f2 = async { ...; 42 }
if (await(f1)) await(f2) else 0
}
This mirrors strongly what I would want the rust code to look like, with the usage of mandatory delimiters, and, as such, I would like to agree with the others on staying with the current syntax of await!()
. Early Rust was symbol heavy, and was moved away from for good reason, I presume. The use of syntactic sugar in the form of a postfix operator (or what have you) is, as always, backwards compatible, and the clarity of await!(future)
is unambiguous. It also mirrors the progression we had with try!
, as previously mentioned.
A benefit of keeping it as a macro is that it is more immediately obvious at a glance that it is a language feature rather than a normal function call. Without the addition of the !
, the syntax highlighting of the editor/viewer would be the best way to be able to spot the calls, and I think relying on those implementations is a weaker choice.
My two cents (not a regular contributor, fwiw) I'm most partial to copying the model of try!
. It's been done once before, it worked well and after it became very popular there were enough users to consider a postfix operator.
So my vote would be: stabilize with await!(...)
and punt on a postfix operator for nice chaining based on a poll of Rust developers. Await is a keyword, but the !
indicates that it's something "magic" to me and the parenthesis keep it unambiguous.
Also a comparison:
Postfix | Expression |
---|---|
.await |
client.get("https://my_api").send().await?.json().await? |
.await! |
client.get("https://my_api").send().await!?.json().await!? |
.await() |
client.get("https://my_api").send().await()?.json().await()? |
^ |
client.get("https://my_api").send()^?.json()^? |
# |
client.get("https://my_api").send()#?.json()#? |
@ |
client.get("https://my_api").send()@?.json()@? |
$ |
client.get("https://my_api").send()$?.json()$? |
My third cent is that I like @
(for "await") and #
(to represent multi-threading/concurrency).
I like postfix @
too! I think it's actually not a bad option, even though there seems to be some sentiment that it isn't viable.
?
and @
would be very similar, so learning @
after learning ?
shouldn't be such a leapI'm very much in favor of the await? foo
syntax, and I think it is similar to some syntax seen in math, where eg. sin² x can be used to mean (sin x)². It looks a bit awkward at first, but I think it is very easy to get used to.
As said above, I'm favorable with adding await!()
as a macro, just like try!()
, for now and eventually deciding how to postfix it. If we can keep in mind support for a rustfix that automatically converts await!()
calls to the postfix await that's yet to be decided, even better.
The postfix keyword option is a clear winner to me.
There is no precedence/ordering issue, yet order could still be made explicit with parentheses. But mostly no need for excessive nesting (similar argument for preferring postfix '?' as replacement of 'try()!').
It looks good with multi-line chaining (see earlier comment by @earthengine), and again, there is no confusion regarding ordering or what is being awaited. And no nesting/extra parentheses for expressions with multiple uses of await:
let x = x.do_something() await
.do_another_thing() await;
let x = x.foo(|| ...)
.bar(|| ...)
.baz() await;
It lends itself to a simple await!() macro (see earlier comment by @novacrazy):
macro_rules! await {
($e:expr) => {{$e await}}
}
Even single-line, naked (without the '?'), postfix await keyword chaining doesn't bother me because it reads left to right and we are awaiting the return of a value that the subsequent method then operates on (though I would just prefer multi-line rustfmt'ed code). The space breaks up the line and is enough of a visual indicator/cue that awaiting is happening:
client.get("https://my_api").send() await.unwrap().json() await.unwrap()
To suggest another candidate that I have not seen put forward yet (maybe because it would not be parseable), what about a fun double-dot '..' postfix operator? It reminds me that we are waiting for something (the result!)...
client.get("https://my_api").send()..?.json()..?
I like postfix
@
too! I think it's actually not a bad option, even though there seems to be some sentiment that it isn't viable.
- @ for await is a nice and easy to remember mnemonic
?
and@
would be very similar, so learning@
after learning?
shouldn't be such a leap- It aids scanning a chain of expressions left to right, without having to scan forward to find a closing delimiter in order to understand an expression
I'm not a fan of using @ for await. It's awkward to type on a fin/swe layout keyboard since I have to hit alt-gr with my right thumb and then hit key 2 on the number row. Also, @ has a well established meaning (at) so I don't see why we should conflate the meaning of it.
I'd much rather just simply type await
, it's faster since it doesn't require any keyboard acrobatics.
Here's my own, very subjective, assessment. I've also added future@await
, which seems interesting to me.
syntax | notes |
---|---|
await { f } |
strong:
|
await f |
strong:
|
fut.await fut.await() fut.await!() |
strong:
|
fut(await) |
strong:
|
f await |
strong:
|
f@ |
strong:
|
f@await |
strong:
|
.
and other await
ssyntax | fam | obv | vrb | vis | cha | grp | fwd |
---|---|---|---|---|---|---|---|
await!(fut) |
++ | + | -- | ++ | -- | 0 | ++ |
await { fut } |
++ | ++ | -- | ++ | -- | 0 | + |
await fut |
++ | - | + | ++ | - | 0 | - |
fut.await |
0 | -- | + | ++ | ++ | + | - |
fut.await() |
0 | -- | - | ++ | ++ | + | - |
fut.await!() |
0 | 0 | -- | ++ | ++ | + | - |
fut(await) |
- | -- | 0 | ++ | ++ | + | - |
fut await |
-- | -- | + | ++ | ++ | -- | - |
fut@ |
- | - | ++ | -- | ++ | ++ | -- |
fut@await |
- | 0 | + | ++ | ++ | ++ | 0 |
It does feel to me like we should mirror try!()
syntax in the first cut and get some real usage out of using await!(expr)
before introducing some other syntax.
However, if/when we do construct an alternate syntax..
I think that @
looks ugly, "at" for "async" doesn't feel that intuitive to me, and the symbol is already used for pattern matching.
async
prefix without parens leads to non obvious precedence or surrounding parens when used with ?
(which will be often).
Postfix .await!()
chains nicely, feels fairly immediately obvious in its meaning, includes the !
to tell me its going to do magic, and is less novel syntactically, so of the "next cut" approaches I personally would favour this one. That said, for me it remains to be seen how much it would improve real code over the first cut await! (expr)
.
I prefer the prefix operator for simple cases:
let result = await task;
It feels way more natural taking into account that we don't write out the type of result, so await helps mentally when reading left-to-right to understand that result is task with the await.
Imagine it like this:
let result = somehowkindoflongtask await;
until you don't reach the end of the task you don't realize that the type that it returns has to be awaited. Keep in mind too (although this is subject to change and not directly linked to the future of the language) that IDEs as Intellij inline the type (without any personalization, if that is even possible) between the name and the equals.
Picture it like this:
That doesn't mean that my opinion is one hundred percent towards prefix. I highly prefer the postfix version of future when results are involved, as that feels way more natural. Without any context I can easily tell which of these means what:
future await?
future? await
Instead look at this one, which of these two is true, from the point of view of a newbie:
await future? === await (future?)
await future? === (await future)?
I am in favor of the prefix keyword: await future
.
It is the one used by most of the programming languages that have async/await, and is therefore immediately familiar to people that know one of them.
As for the precedence of await future?
, what is the common case?
Result<Future>
that has to be awaited.Result
: Future<Result>
.I think the second case is much more common when dealing with typical scenarios, since I/O operations might fail. Therefore:
await future? <=> (await future)?
In the less common first case, it is acceptable to have parentheses: await (future?)
. This might even be a good use for the try!
macro if it hadn't been deprecated: await try!(future)
. This way, the await
and question mark operator are not on different sides of the future.
Why not take await
as first async
function parameter?
async fn await_chain() -> Result<usize, Error> {
let _ = partial_computation(await)
.unwrap_or_else(or_recover)
.run(await)?;
}
client.get("https://my_api")
.send(await)?
.json(await)?
let output = future
.run(await);
Here future.run(await)
is alternative to await future
.
It could be just regular async
function that takes future and simply runs await!()
macro on it.
C++ (Concurrency TR)
auto result = co_await task;
This is in the Coroutines TS, not concurrency.
Before commenting in this thread, please check https://github.com/rust-lang/rust/issues/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 https://github.com/rust-lang/rust/issues/57640#issuecomment-456617889, which was followed by three great summary comments, the latest of which were https://github.com/rust-lang/rust/issues/57640#issuecomment-457101180. (Thanks, @traviscross!)