rust-lang/rust

Resolve `await` syntax

cramertj opened this issue Β· 512 comments

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!)

I thought it might be useful to write up how other languages handle an await construct.


Kotlin

val result = task.await()

C#

var result = await task;

F#

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C++ (Coroutines TR)

auto result = co_await task;

Hack

$result = await task;

Dart

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 #50547, could you summarize the arguments made there to make it easier to not repeat them here? (Maybe add them to the OP here?)

chpio commented

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(?)
  • or 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:

  • Compared to .await!() it does not allude to a member that could have other uses.
  • It follows natural precedence of calls, such as chaining and use of ?. 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.
  • This could generalize to generators and streams, as well as generators that expect more arguments to be provided on resumption. In essence, this behaves as an FnOnce while Streams would behave like a FnMut. Additional arguments may also be accomodated easily.
  • For those who have used current 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.
  • It does use a function-like syntax, though this argument is only good if you agree with me πŸ˜„

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.
  • 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. 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.
  • Interference with other function call proposals?
  • Your own? You need not like it, it just felt like a waste to not at least propose this terse post-fix syntax.
Pauan commented

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.

Pauan commented

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.

chpio commented

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:

  • Prefix await with mandatory delimiters. Here this is also a decision of what delimiters (either braces or parens or accepting both; all of these have their own pros and cons). That is, await(future) or await { future }. This completely solves the precedence problems, but is syntactically noisy and both delimiter options present possible sources of confusion.
  • Prefix await with the "useful" precedence regarding ?. (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.
  • Prefix await with the "obvious" precedence regarding ?. (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.
  • Postfix await with the syntax space await. This solves the precedence problem by having a clear visual ordering between the two operators. I feel uneasy about this solution in a lot of respects.

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.

mzji commented

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.

chpio commented

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 had try!()

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())?;
  • Postfix keyword foo() await?
    • Example: client.get("https://my_api").send() await?.json() await?
    • Concern Resolution
      Chaining without ? may be confusing or disallowed
  • Postfix field foo().await?
    • Example: client.get("https://my_api").send().await?.json().await?
    • Concern Resolution
      Looks like a field
  • Postfix method foo().await()?
    • Example: 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
  • Postfix call foo()(await)?
    • Example: client.get("https://my_api").send()(await)?.json()(await)?
    • Concern Resolution
      Can be confused with actual argument keyword+highlighting+not overlapping
  • Postfix macro foo().await!()?
    • Example: 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).

casey commented

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 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:
  • very straightforward
  • parallels for, loop, async etc.
weak:
  • very verbose (5 letters, 2 braces, 3 optional, but probably linted spaces)
  • chaining results in many nested braces (await { await { foo() }?.bar() }?)
await f strong:
  • parallels await syntax from Python, JS, C# and Dart
  • straightforward, short
  • both useful precedence vs. obvious precedence behave nicely with ? (await fut? vs. await? fut)
weak:
  • ambiguous: useful vs. obvious precedence must be learned
  • chaining is also very cumbersome (await (await foo()?).bar()? vs. await? (await? foo()).bar())
fut.await
fut.await()
fut.await!()
strong:
  • allows very easy chaining
  • short
  • nice code completion
weak:
  • fools users into thinking it's a field/function/macro defined somewhere. Edit: I agree with @jplatte that await!() feels the least magical
fut(await) strong:
  • allows very easy chaining
  • short
weak:
  • fools users into thinking there's an await variable defined somewhere and that futures can be called like a function
f await strong:
  • allows very easy chaining
  • short
weak:
  • parallels nothing in Rust's syntax, not obvious
  • my brain groups the client.get("https://my_api").send() await.unwrap().json() await.unwrap() into client.get("https://my_api").send(), await.unwrap().json() and await.unwrap() (grouped by first, then .) which is not correct
  • for Haskellers: looks like currying but isn't
f@ strong:
  • allows very easy chaining
  • very short
weak:
  • looks slightly awkward (at least at first)
  • consumes @ which might be better suited for something else
  • might be easy to overlook, especially in large expressions
  • uses @ in a different way than all other languages
f@await strong:
  • allows very easy chaining
  • short
  • nice code completion
  • await doesn't need to become a keyword
  • forwards-compatible: allows new postfix operators to be added in the form @operator. For example, the ? could have been done as @try.
  • my brain groups the client.get("https://my_api").send()@await.unwrap().json()@await.unwrap() into the correct groups (grouped by . first, then @)
weak:
  • uses @ in a different way than all other languages
  • might incentivice adding too many unnecessary postfix operators

My scores:

  • familiarity (fam): How close this syntax is to known syntaxes (Rust and others, such as Python, JS, C#)
  • obviousness (obv): If you were to read this in someone else's code for the first time, would you be able to guess the meaning, precedence etc.?
  • verbosity (vrb): How many characters it takes to write
  • visibility (vis): How easy it is to spot (vs. to overlook) in code
  • chaining (cha): How easy it is to chain it with . and other awaits
  • grouping (grp): Whether my brain groups the code into the correct chunks
  • forwards-compatibility (fwd): Whether this allows to be adjusted later in a non-breaking manner
syntax 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
jsdw commented

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:
6voler6ykj

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?

  • A function returning a Result<Future> that has to be awaited.
  • A future that has to be awaited which returns a 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.

I60R commented

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.

I60R commented

Another option could be using become keyword instead of await:

async fn become_chain() -> Result<usize, Error> {
    let _ = partial_computation_future(become)
        .unwrap_or_else(or_recover_future)
        .start(become)?;
}

client.get("https://my_api")
    .send_future(become)?
    .json_future(become)?

let output = future.start(become);
L-as commented

become could be a keyword for guaranteed TCO though

Thanks @EyeOfPython for that overview. That's especially useful for people just now joining the bike-shed.

Personally I'd hope we stay away from f await, just because it's very un-rustic syntax wise and makes it seem kinda special and magic. It would become one of these things that users new to Rust are going to be confused about a lot and I feel it doesn't add that much clarity, even to Rust veterans, to be worth that.

@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

@Nemo157 I agree that an await! macro would be magic, but I would argue that this is not a problem. There are multiple such macros in std already, e.g. compile_error! and I haven't seen anyone complain about them. I think it's normal to use a macro only understanding what it does, not how it does it.

I agree with previous commenters that postfix would be the most ergonomic, but I would rather start out with prefix-macro await!(expr) and potentially transition to postfix-macro once that is a thing rather than have expr.await (magic compiler-builtin field) or expr.await() (magic compiler-builtin method). Both of those would introduce a completely new syntax purely for this feature, and IMO that just makes the language feel inconsistent.

@EyeOfPython Mind adding future(await) to your lists and table? All the positives of your assessment of future.await() seem to transfer without the weakness

Since some have argued that despite syntax highlighting, foo.await looks too much like a field access, we could change the token . to # and instead write foo#await. For example:

let foo = alpha()#await?
    .beta#await
    .some_other_stuff()#await?
    .even_more_stuff()#await
    .stuff_and_stuff();

To illustrate how GitHub would render this with syntax highlighting, let's replace await with match since they are of equal length:

let foo = alpha()#match?
    .beta#match
    .some_other_stuff()#match?
    .even_more_stuff()#match
    .stuff_and_stuff();

This seems both clear and ergonomic.

The rationale for # rather some other token is not specific, but the token is quite visible which helps.

I60R commented

So, another concept: if future was reference:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   *self.logger.log("beginning service call");
   let output = *service.exec(); // Actually wait for its result
   *self.logger.log("foo executed with result {}.", output);
   output
}

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)
}

async fn await_chain() -> Result<usize, Error> {
    *(*partial_computation()).unwrap_or_else(or_recover);
}

(*(*client.get("https://my_api").send())?.json())?

let output = *future;

This would be really ugly and inconsistent. Let at least disambiguate that syntax:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   $self.logger.log("beginning service call");
   let output = $service.exec(); // Actually wait for its result
   $self.logger.log("foo executed with result {}.", output);
   output
}

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)
}

async fn await_chain() -> Result<usize, Error> {
    $($partial_computation()).unwrap_or_else(or_recover);
}

($($client.get("https://my_api").send())?.json())?

let output = $future;

Better, but still ugly (and yet worse it makes github's syntax highlighting). However, to deal with that let introduce ability to delay prefix operator:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.$log("beginning service call");
   let output = service.$exec(); // Actually wait for its result
   self.logger.$log("foo executed with result {}.", output);
   output
}

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)
}

async fn await_chain() -> Result<usize, Error> {
    ($partial_computation()).$unwrap_or_else(or_recover);
}

client.get("https://my_api").$send()?.$json()?

let output = $future;

This exactly what I want! Not only for await ($) but as well for deref (*) and negate (!).

Some caveats in this syntax:

  1. Operator precedence: for me it's obvious but for other users?
  2. Macros interop: would $ symbol cause problems here?
  3. Expression inconsistency: leading prefix operator applies to the whole expression while delayed prefix operator applies only to . delimited expression (await_chain fn demonstrate that), is that confusing?
  4. Parsing and implementation: is that syntax valid at all?

@Centril
IMO # is too close to the raw literal syntax r#"A String with "quotes""#

IMO # is too close to the raw literal syntax r#"A String with "quotes""#

It seems quite clear from the context what the difference is in this case.

L-as commented

@Centril
It does, but the syntax is also really foreign to the style Rust uses IMHO. It does not resemble any existing syntax with a similar function.

@laaas Neither did ? when it was introduced. I would be happy to go with .await; but others seem unhappy with that so I am trying to find something else that works (i.e. is clear, ergonomic, sufficiently easy to type, chainable, has good precedence) and foo#await seems to satisfy all of that.

L-as commented

? has a bunch of other good points though, while #await seems rather arbitrary. In any case, if you want something like .await, why not .await!?

@Centril That was the main rationale behind future(await), to avoid the field access while not having to add any extra operators or foreign syntax.

while #await seems rather arbitrary.

# is arbitrary, yes -- is that make or break? When new syntax is invented at some point it has to be arbitrary because someone thought it looked good or made sense.

why not .await!?

That's equally arbitrary but I have no opposition to it.

@Centril That was the main rationale behind future(await), to avoid the field access while not having to add any extra operators or foreign syntax.

Instead it looks like function application where you are passing await to future; that strikes me as more confusing than "field access".

Instead it looks like function application where you are passing await to future; that strikes me as more confusing than "field access".

To me that was part of the conciseness. Await is in many aspects like a function call (callee executes between you and your result), and passing a keyword to that function made it clear that this is a different sort of call. But I see how it could be confusing that the keyword is similar to any other argument. (Minus syntax highlighting and compiler error messages trying to reinforce that message. Futures can not be called any other way than being awaited and vice versa).

L-as commented

@Centril

Being arbitrary isn't a negative thing, but having resemblance to an existing syntax is a positive thing. .await! isn't as arbitrary, since it's not entirely new syntax; after all, it's just a post-fix macro named await.

To clarify / add to the point about new syntax specifically for await, what I mean by that is introducing new syntax that's unlike the existing syntax. There is a lot of prefix keywords, some of which have been added after Rust 1.0 (e.g. union), but AFAIK not a single postfix keyword (no matter if separated by a space, a dot, or something else). I also can't think of any other language that has postfix keywords.

IMHO the only postfix syntax that doesn't significantly increase Rust's strangeness is the postfix macro, becauses it mirrors method calls but can clearly be identified as a macro by anyone who has seen a Rust macro before.

I also liked the standard await!() macro. It's just clear and simple.
I prefer a field-like access (or any other postfix) to co-exist as awaited.

  • Await feels you'll be awaiting for what comes next/right. Awaited, to what came before/left.
  • Coherent to the cloned() method on iterators.

And I also liked the @? as ?-like sugar.
This is personal, but I actually prefer the &? combination, because @ tends to be too tall and undershoot the line, which is very distractible. & is also tall (good) but doesn't undershoots (also good). Unfortunately & already has a meaning, although it would always be followed by an ?.. I guess?

Eg.

Lorem@?
  .ipsum@?
  .dolor()@?
  .sit()@?
  .amet@?
Lorem@?.ipsum@?.dolor()@?.sit()@?.amet@?

Lorem&?
  .ipsum&?
  .dolor()&?
  .sit()&?
  .amet&?
Lorem&?.ipsum&?.dolor()&?.sit()&?.amet&?

For me, the @ feels like a bloated character, distracts the reading flow. On the other hand, &? feels pleasant, doesn't distracts (my) reading and has a nice top space between the & and the ?.

L-as commented

I personally think a simple await! macro should be used. If post-fix macros get into the language, then the macro could simply be expanded, no?

@laaas As much as I would like for there to be, there are no postfix macros in Rust yet, so they are new syntax. Also note that foo.await!.bar and foo.await!().bar aren't the same surface syntax. In the latter case there would be an actual postfix and builtin macro (which entails giving up on await as a keyword).

@jplatte

There is a lot of prefix keywords, some of which have been added after Rust 1.0 (e.g. union),

union is not a unary expression operator so it is irrelevant in this comparison. There are exactly 3 stable unary prefix operators in Rust (return, break, and continue) and all of them are typed at !.

I60R commented

Hmm... With @ my previous proposal looks slightly better:

client.get("https://my_api").@send()?.@json()?

let output = @future;

let foo = (@alpha())?
    .@beta
    .@some_other_stuff()?
    .@even_more_stuff()
    .stuff_and_stuff();
L-as commented

@Centril That is my point, since we do not have post-fix macros yet, it should simply be await!(). Also I meant .await!() FYI. I do not think await needs to be a keyword, though you could reserve it if you find it problematic.

union is not a unary expression operator so it is irrelevant in this comparison. There are exactly 3 stable unary prefix operators in Rust (return, break, and continue) and all of them are typed at !.

This still means that the prefix-keyword variant fits into the group of unary expression operators, even if typed differently. The postfix-keyword variant is a much larger divergence from existing syntax, one that is too large in relation to await's importance in the language in my opinion.

Regarding prefix macro and possibility of transitioning to post-fix: await needs to remain a keyword for this to work. The macro would then be some special lang item where it's allowed to use a keyword as the name without an extra r#, and r#await!() could but probably should not invoke the same macro. Other than that it seems like the most pragmatic solution to make it available.

That is, keep await as a keyword but make await! resolve to a lang-item macro.

L-as commented

@HeroicKatora why does it need to remain a keyword for it to work?

@laaas Because if we want to keep the possibility of transitioning open, we need to remain open for future post-fix syntax that uses it as a keyword. Therefore, we need to keep the keyword reservation so that we don't need an edition break for the transition.

@Centril

Also note that foo.await!.bar and foo.await!().bar aren't the same surface syntax. In the latter case there would be an actual postfix and builtin macro (which entails giving up on await as a keyword).

Could this not be solved by making the keyword await combined with ! resolve to an internal macro (which can not be defined through normal means)? Then it remains a keyword but resolves to a macro in otherwise macro syntax.

L-as commented

@HeroicKatora Why would await in x.await!() be a reserved keyword?

It would not be, but if we keep post-fix unresolved, it need not be the solution we arrive at in later discussions. If that were the unique agreed upon best possibility, then we should adopt this exact post-fix syntax in the first place.

This is another time that we come across something that works a lot better as a postfix operator. The big example of this is try! which we eventually gave its own symbol ?. However I think this is not the last time where a postfix operator is more optimal and we can not give everything its own special charachter. So I think we should at least not start with @. It would be a lot better if we would have a way to do this kinds of things. That is why I support the postfix macro style .await!().

let x = foo().try!();
let y = bar().await!();

But for this to make sense postfix macros them self would have to be introduced. Therefore I think it would be best to start with a normal await!(foo) macro syntax. We could later expand this to either foo.await!() or even foo@ if we really feel this is important enough to warrant its own symbol.
That this macro would need a bit of magic is not new to std and to me is not a big problem.
As @jplatte put it:

@Nemo157 I agree that an await! macro would be magic, but I would argue that this is not a problem. There are multiple such macros in std already, e.g. compile_error! and I haven't seen anyone complain about them. I think it's normal to use a macro only understanding what it does, not how it does it.

@HeroicKatora I've added fut(await) to the list and ranked it according to my opinion.

If anyone feels like my scoring is off, please tell me!

Syntax-wise, I think .await!() is clean and allows for chaining while not adding any oddities to the syntax.

However, if we ever get "real" postfix macros, it will be a little weird, because presumably .r#await!() could be shadowed, while .await!() couldn't.

I'm pretty strongly against the "postfix keyword" option (separated with a space like: foo() await?.bar() await?), because I find the fact that the await is joined to the following part of the expression, and not the part that it operates on. I would prefer pretty much any symbol other than whitespace here, and would even prefer prefix syntax over this despite it's disadvantages with long chains.

I think "obvious precedence" (with the await? sugar) is clearly the best prefix option, as mandatory delimiters is a pain for the very common case of awaiting a single statement, and "useful precedence" is not at all intuitive, and thus confusing.

Syntax-wise, I think .await!() is clean and allows for chaining while not adding any oddities to the syntax.

However, if we ever get "real" postfix macros, it will be a little weird, because presumably .r#await!() could be shadowed, while .await!() couldn't.

If we get postfix macros and we use .await!() then we can unreserve await as a keyword and just make it a postfix macro. The implementation of this macro would still require a bit of magic of cource but it would be a real macro just like compiler_error! is today.

@EyeOfPython Could you maybe explain in detail which changes you consider in forward-compatibility? I'm unsure in which way fut@await would rate higher than fut.await!() and await { future } lower than await!(future). The verbosity column also seems a bit strange, some expressions are short but have lower rating, does it consider chained statements, etc. Everything else seems balanced and like the most well condensed extensive assessment so far.

After reading this discussion, it seems that lots of people want to add a normal await!() macro and figure out the postfix version later. That is assuming we actually want a postfix version, which I'll take as true for the rest of this comment.

So I'd just like to poll the opinion of everyone here: Should we all agree on the await!(future) syntax FOR NOW? The pros are that there's no parsing ambiguity with that syntax, and it's also a macro, so no changing the language's syntax to support this change. The cons are that it will look ugly for chaining, but that doesn't matter since this syntax can be easily replaced with a postfix version automatically.

Once we settle that down and get it implemented into the language, we can then continue the discussion for postfix await syntax with hopefully more mature experiences, ideas and possibly other compiler features.

@HeroicKatora I rated it higher as it could free await to be used as ordinary identifier naturally and it would allow other postfix operators to be added, whereas I think fut.await!() would better be if await was reserved. However, I'm unsure if this is reasonable, it also seems definitely valid that fut.await!() could be higher.

For await { future } vs await!(future), the latter one keeps the option open to change to almost any of the other options, whereas the first one only really allows either of the await future variants (as laid out in @withoutboats' blog entry). So I think it should definitely be fwd(await { future }) < fwd(await!(future)).

As for the verbosity, you are correct, after splitting cases into subgroups I didn't re-evaluate the verbosity, which should be the most objective one of all.

I'll edit it to take your comments into account, thank you!

Stabilizing await!(future) is about the worst possible option I can imagine:

  1. It means we have to unreserve await which further means that future language design gets harder.
  2. It is knowingly taking the same route as with try!(result) which we deprecated (and which requires writing r#try!(result) in Rust 2018).
    • If we know that await!(future) is bad syntax we eventually mean to deprecate, this is willfully creating technical debt.
    • Moreover, try!(..) is defined in Rust whereas await!(future) cannot be and would instead be compiler magic.
    • Deprecating try!(..) was not easy and took a toll socially. Going through that ordeal again doesn't seem appealing.
  3. This would use macro syntax for one a central and important part of the language; that seems distinctly not first-class.
  4. await!(future) is noisy; unlike await future you need to write !( ... ).
  5. Rust APIs, and especially the standard library, are centered around method call syntax. For example, it is commonplace to chain methods when dealing with Iterators. Similar to await { future } and await future, the await!(future) syntax will make method chaining difficult and induce temporary let bindings. This is bad for ergonomics and in my view for readability.

@Centril I'd like to agree but there are some open questions. Are you sure about 1.? If we make it 'magic' could we not make it even more magic by allowing this to refer to a macro without dropping it as a keyword?

I joined Rust too late to evaluate the social perspective of 2.. Repeating a mistake doesn't sound appealing but as some code still was not converted from try! it should be evident that it was a solution. That brings up the question of whether we intend to have async/await as an incentive to migrate to edition 2018 or rather be patient and not repeat this.

Two other (imho) central features very much like 3.: vec![] and format!/println!. The former very much because there is no stable boxed construction afaik, the latter due to format string construction and not having dependently typed expressions. I think these comparisons also partially put 4. into another perspective.

I oppose any syntax that doesn't read somewhat like english. IE, "await x" reads something like english. "x#!!@!&" does not. "x.await" reads tantalizingly like english, but it won't when x is a non-trivial line, like a member function call with long names, or a bunch of chained iterator methods, etc.

More specifically, I support "keyword x", where keyword is probably await. I come from using both the C++ coroutines TS and unity's c# coroutines, which both use a syntax very similar to that. And after years of using them in production code, my belief is that knowing where your yield points are, at a glance, is absolutely critical. When you skim down the indent line if your function, you can pick out every co_await/yield return in a 200-line function in a matter of seconds, with no cognitive load.

The same is not true of the dot operator with await afterwards, or some other postfix "pile of symbols" syntax.

I believe that await is a fundamental a control flow operation. It should be given the same level of respect as 'if, while, match, and return. Imagine if any of those were postfix operators - reading Rust code would be a nightmare. Like with my argument for await, as it is you can skim the indent line of any Rust function and immediately pick out all of the control flow. There are exceptions, but they're exceptions, and not something we should strive for.

I am agree with @ejmahler. We should not forget of other side of development - code review. File with source code is much more often being read then wrote, hence I thing it should be easier to read-and-understand then to write. Finding the yielding points is really important on code review. And I personally would vote for Useful precedence.
I believe that this:

...
let response = await client.get("https://my_api").send()?;
let body: MyResponse = await response.into_json()?;

is easier for understanding rather then this:

...
let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;

@HeroicKatora

@Centril I'd like to agree but there are some open questions. Are you sure about 1.? If we make it 'magic' could we not make it even more magic by allowing this to refer to a macro without dropping it as a keyword?

Technically? Possibly. However, there should be strong justification for special cases and in this case having magic upon magic doesn't seem justified.

That brings up the question of whether we intend to have async/await as an incentive to migrate to edition 2018 or rather be patient and not repeat this.

I don't know if we ever said we intended for new module system, async/await, and try { .. } to be incentives; but irrespective of our intent they are, and I think that's a good thing. We want people to eventually start using new language features to write better and more idiomatic libraries.

Two other (imho) central features very much like 3.: vec![] and format!/println!. The former very much because there is no stable boxed construction afaik,

The former exists, and is written vec![1, 2, 3, ..], to mimic array literal expressions, e.g. [1, 2, 3, ..].

@ejmahler

"x.await" reads tantalizingly like english, but it won't when x is a non-trivial line, like a member function call with long names, or a bunch of chained iterator methods, etc.

What's wrong with a bunch of chained iterator methods? That's distinctly idiomatic Rust.
The rustfmt tool will also format method chains on different lines so you get (again using match to show the syntax highlighting):

let foo = alpha().match?  // or `alpha() match?`, `alpha()#match?`, `alpha().match!()?`
    .beta
    .some_other_stuff().match?
    .even_more_stuff().match
    .stuff_and_stuff();

If you read .await as "then await" it reads perfectly, at least to me.

And after years of using them in production code, my belief is that knowing where your yield points are, at a glance, is absolutely critical.

I don't see how postfix await negates that, especially in the rustfmt formatting above. Moreover, you can write:

let foo = alpha().match?;
let bar = foo.beta.some_other_stuff().match?;
let baz = bar..even_more_stuff().match;
let quux = baz.stuff_and_stuff();

if you fancy that.

in a 200-line function in a matter of seconds, with no cognitive load.

Without knowing too much about the particular function, it seems to me that 200 LOC probably violates the single responsibility principle and does too much. The solution is to make it do less and split it up. In fact, I think that is the most important thing for maintainability and readability.

I believe that await is a fundamental a control flow operation.

So is ?. In fact, await and ? are both effectful control flow operations that say "extract value out of context". In other words, in the local context, you can imagine these operators having the type await : impl Future<Output = T> -> T and ? : impl Try<Ok = T> -> T.

There are exceptions, but they're exceptions, and not something we should strive for.

And the exception here is ? ?

@andreytkachenko

I am agree with @ejmahler. We should not forget of other side of development - code review. File with source code is much more often being read then wrote, hence I thing it should be easier to read-and-understand then to write.

The disagreement is around what would be best for readability and ergonomics.

is easier for understanding rather then this:

...
let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;

This is not how it would be formatted; running it through rustfmt gets you:

let body: MyResponse = client
    .get("https://my_api")
    .send()
    .match?
    .into_json()
    .match?;

@ejmahler @andreytkachenko I agree with @Centril here, the biggest change (some may say improvement, I would not) you gain from prefix syntax is that users are incentivized to split their statements over multiple lines because everything else is unreadable. That is not Rust-y and usual formatting rules make up for this in post-fix syntax. I also consider the yield-point to be more obscure in prefix syntax because await is not actually placed at the code point where you yield, rather opposed to it.

If you go this way, let's experiment for the sake of spelling it out, rather with await as a replacement for let in the spirit of @Keruspe 's idea to really enforce this. Without the other syntax extensions because they seem like a stretch.

await? response = client.get("https://my_api").send();
await? body: MyResponse = response.into_json();

But for none of these I see enough benefit to explain their composability loss and complications in the grammar.

Hm... is it desirable to have both a prefix and suffix form of await? Or just the suffix form?

@ejmahler We disagree re. "hiding"; the same arguments were made wrt. the ? operator:

The ? operator is very short, and has been criticised in the past to "hide" a return. Most times code is read, not written. Renaming it to ?! will make it two times as long and therefore harder to overlook.

Nevertheless, we ultimately stabilized ? and since then I think the prophecy has failed to materialize.
To me, postfix await follows natural reading order (at least for left-to-right language speakers). In particular, it follows data-flow order.

Not to mention syntax highlighting: anything await-related can be highlighted with a bright color, so they can be found at a glance. So even if we had a symbol instead of the actual await word, it would still be very readable and findable in syntax highlighted code. With that said, I still prefer the use of the await word just for grepping reasons - it's easier to grep the code for anything that's being awaited if we only use the await word instead of a symbol like @ or #, whose meaning is grammar dependent.

y'all this isn't rocket science

let body: MyResponse = client.get("https://my_api").send()...?.into_json()...?;

postfix ... is extremely readable, hard to miss at a glance and super intuitive since you naturally read it as the code kind of trailing off while it waits for the result of the future to become available. no precedence/macro shenanigans necessary and no extra line noise from unfamiliar sigils, since everybody has seen ellipses before.

(apologies to @solson)

@ben0x539 Does that mean I can access a member of my result like future()....start? Or await a range result like so range().....? And how exactly to you mean no precedence/macro shenanigans necessary and since currently ellipsis .. requires being a binary operator or paranthesis on the right and this is awfully close at a glance.