Rust: Resolve `await` syntax

Created on 15 Jan 2019  ·  512Comments  ·  Source: rust-lang/rust

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

A-async-await AsyncAwait-Focus C-tracking-issue T-lang

Most helpful comment

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.

All 512 comments

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

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.

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> 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.

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

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 |

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.

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.

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

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.

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.

@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.

? 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).

@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 ?.

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

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();

@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.

@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.

@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.

A recurring issue I see discussed here is about chaining and how to use
await in chaining expressions. Maybe there could be another solution ?

If we want not to use await for chaining and only use await for
assignments, we could have something like just replacing let with await :
await foo = future

Then for chaining we could imagine some kind of operation like await res = fut1 -> fut2 or await res = fut1 >>= fut2.

The only case missing is waiting and returning the result, some shortcut
for await res = fut; res.
This could be easily done with a plain await fut

I don’t think I’ve seen another proposal like this one (at least for the
chaining), so dropping that here as I think it would be nice to use.

@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?

Chaining method calls together is idiomatic specifically when talking about
iterators and to a much lesser extent options, but aside from that, rust is
an imperative language first and a functional language second.

I’m not arguing that a postfix is literally incomprehensible, I’m makin a
cognitive load argument that hiding a top-tier control flow operation,
carrying the same importance as ‘return’, increases cognitive load when
compared to putting it as close to the beginning of the line - and I’m
making this argument based on years of production experience.

On Sat, Jan 19, 2019 at 11:59 AM Mazdak Farrokhzad notifications@github.com
wrote:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/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 https://github.com/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 -> T and ? : impl Try -> T.

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

And the exception here is ? ?

@andreytkachenko https://github.com/andreytkachenko

I am agree with @ejmahler https://github.com/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; the idiomatic rustfmt formatting
is:

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


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba
.

@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.

Yes, the ? Operator exists. I already acknowledged that there were
exceptions. But it’s an exception. The vast majority of control flow in any
Rust program happens via prefix keywords.

On Sat, Jan 19, 2019 at 1:51 PM Benjamin Herr notifications@github.com
wrote:

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.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455818177,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba
.

@HeroicKatora That looks a bit artificial but yeah sure. I meant that since it's a postfix operation, like the other postfix solutions suggested, it avoids the need for counterintuitive precedence for await x?, and it's not a macro.

@ejmahler

Yes, the ? Operator exists. I already acknowledged that there were exceptions. But it’s an exception.

There are two expression forms that fit keyword expr, namely return expr and break expr. The former is more common than the latter. The continue 'label form doesn't really count since, while it is an expression, it isn't of form keyword expr. So now you have 2 whole prefix keyword unary expression forms and 1 postfix unary expression form. Before we even take into account that ? and await are more similar than await and return are, I'd hardly call return/break expr a rule for ? to be an exception against.

The vast majority of control flow in any Rust program happens via prefix keywords.

As aforementioned, break expr isn't all that common (break; is more typical and return; are more typical and they are not unary expression forms). What remains is early return expr;s and it doesn't seem at all clear to me that this is vastly more common than match, ?, just nested if lets and elses, and for loops. Once try { .. } stabilizes I'd expect ? to be used even more.

@ben0x539 I think we should reserve ... for variadic generics, once we're ready to have them

I really like the idea of innovating with postfix syntax here. It makes a lot more sense with the flow and I remember how much better code turned when we went from prefix try! to postfix ?. I think there is a lot of people that made the experience in the Rust community of how much improvements to code that made.

If we don't like the idea of .await I'm sure some creativity can be made to find an actual postfix operator. One example could just be to use ++ or @ for await.

:( I just don’t want to await anymore.

Everyone is comfortable with macro syntax, most people in this thread that start with other opinions seem to end up favoring macro syntax.

Sure it’ll be a “magic macro” but users rarely care about what the macro expansion looks like and for those who do, it is easy enough to explain the nuance in the docs.

Regular macro syntax is kinda like apple pie, it is everyone’s second favorite option but as a result the family’s favorite option[0]. Importantly, like with try! we can always change it later. But most importantly, the sooner we can all agree the sooner we can all start actually using it and be productive!

[0] (Referenced in the first minute) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

match, if, if let, while, while let, and for are all pervasive control flow
that use prefixes. Pretending break and continue are the only control flow
keywords is frustratingly misleading.

On Sat, Jan 19, 2019 at 3:37 PM Yazad Daruvala notifications@github.com
wrote:

:( I just don’t want to await anymore.

Everyone is comfortable with macro syntax, most people in this thread that
start with other opinions seem to end up favoring macro syntax.

Sure it’ll be a “magic macro” but users rarely care about what the macro
expansion looks like and for those who do, it is easy enough to explain the
nuance in the docs.

Regular macro syntax is kinda like apple pie, it is everyone’s second
favorite option but as a result the family’s favorite option[0].
Importantly, like with try! we can always change it later. But most
importantly, the sooner we can all agree the sooner we can all start
actually using it and be productive!

[0] (Referenced in the first minute)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba
.

@mitsuhiko I agree! Postfix feels more rustic due to the chaining. I think the fut@await syntax I proposed is another interesting option that doesn't seem to have as many downsides as other proposals. I'm not sure though if it's too far out there and a more down-to-earth version would be preferable.

@ejmahler

match, if, if let, while, while let, and for are all pervasive control flow that use prefixes. Pretending break and continue are the only control flow keywords is frustratingly misleading.

It's not misleading at all. The relevant grammar for these constructs is roughly:

Expr = kind:ExprKind;
ExprKind =
  | If:{ "if" cond:Cond then:Block { "else" else_expr:ElseExpr }? };
  | Match:{ "match" expr:Expr "{" arms:MatchArm* "}" }
  | While:{ { label:LIFETIME ":" }? "while" cond:Cond body:Block }
  | For:{ { label:LIFETIME ":" }? "for" pat:Pat "in" expr:Expr body:Block }
  ;

Cond =
  | Bool:Expr
  | Let:{ "let" pat:Pat "=" expr:Expr }
  ;

ElseExpr =
  | Block:Block
  | If:If
  ;

MatchArm = pats:Pat+ % "|" { "if" guard:Expr }? "=>" body:Expr ","?;

Here, the forms are if/while expr block, for pat in expr block, and match expr { pat0 => expr0, .., patn => exprn }. There is a keyword that precedes what follows in all of these forms. I guess this is what you mean by "uses prefixes". However, these are all block forms and not unary prefix operators. The comparison with await expr is therefore misleading since there's no consistency or rule to speak of. If you are going for consistency with block forms, then compare that with await block, not await expr.

@mitsuhiko I agree! Postfix feels more rustic due to the chaining.

Rust is dualistic. It supports both imperative and functional approaches. And I think it's fine, because in different cases, each of them can be more suitable.

I don't know. Feels like it would be great to have both:

await foo.bar();
foo.bar().await;

Having used Scala for a while, I was quite fond of many things working like that too. Especially match and if would be a nice to have in postfix positions in Rust.

foo.bar().await.match {
   Bar1(x, y) => {x==y},
   Bar2(y) => {y==7},
}.if {
   bazinga();
}

Summary so far

Option matrices:

Summary matrix of options (using @ as the sigil, but could be mostly anything):

| Name | Future<T> | Future<Result<T, E>> | Result<Future<T>, E> |
|---|---|---|---|
| PREFIX | - | - | - |
| Keyword Macro | await!(fut) | await!(fut)? | await!(fut?) |
| Keyword Function | await(fut) | await(fut)? | await(fut?) |
| Useful Precedence | await fut | await fut? | await (fut?) |
| Obvious Precedence | await fut | await? fut | await fut? |
| POSTFIX | - | - | - |
| Fn With Keyword | fut(await) | fut(await)? | fut?(await) |
| Keyword Field | fut.await | fut.await? | fut?.await |
| Keyword Method | fut.await() | fut.await()? | fut?.await() |
| Postfix Keyword Macro | fut.await!() | fut.await!()? | fut?.await!() |
| Space Keyword | fut await | fut await? | fut? await |
| Sigil Keyword | fut@await | fut@await? | fut?@await |
| Sigil | fut@ | fut@? | fut?@ |

"Sigil Keyword"'s sigil _cannot_ be #, as then you couldn't do it with a future called r. ... as the sigil would not have to change tokenization like my first worry.

More real-life use (PM me other _real_ use cases with multiple await on urlo and I'll add them):

| Name | (reqwest) Client |> Client::get |> RequestBuilder::send |> await |> ? |> Response::json |> ? |
|---|---|
| PREFIX | - |
| Keyword Macro | await!(client.get("url").send())?.json()? |
| Keyword Function | await(client.get("url").send())?.json()? |
| Useful Precedence | (await client.get("url").send()?).json()? |
| Obvious Precedence | (await? client.get("url").send()).json()? |
| POSTFIX | - |
| Fn With Keyword | client.get("url").send()(await)?.json()? |
| Keyword Field | client.get("url").send().await?.json()? |
| Keyword Method | client.get("url").send().await()?.json()? |
| Postfix Keyword Macro | client.get("url").send().await!()?.json()? |
| Space Keyword | client.get("url").send() await?.json()? |
| Sigil Keyword | client.get("url").send()@await?.json()? |
| Sigil | client.get("url").send()@?.json()? |

EDIT NOTE: It has been pointed out to me that it may make sense for Response::json to also return a Future, where send waits for the outgoing IO and json (or other interpretation of the result) waits for the incoming IO. I'm going to leave this example as-is, though, as I think it's meaningful to show that the chaining issue applies even with only one IO await point in the expression.

There seems to be rough consensus that of the prefix options, the obvious precedence (along with the await? sugar) is the most desirable. However, many people have spoken up in favor of a postfix solution, in order to make chaining as above easier. Although the prefix choice has rough consensus, there seems to be no consensus over what postfix solution is best. All of the proposed options lead to easy confusion (mitigated by keyword highlighting):

  • Fn With Keyword => calling a fn with an argument called await
  • Keyword Field => field access
  • Keyword Method => method call
  • Macro (prefix or postfix) => is await a keyword or not?
  • Space Keyword => breaks the grouping in single-line (better over multiple lines?)
  • Sigil => adds new sigils to a language that's already perceived as sigil-heavy

Other more drastic suggestions:

  • Allow both prefix (obvious precedence) and postfix "field" (this could be applied to more keywords like match, if, etc in the future to make this a generalized pattern, but is an unnecessary addendum to this debate) [[reference](https://github.com/rust-lang/rust/issues/57640#issuecomment-455827164)]
  • await in patterns to resolve futures (no chaining at all) [[reference](https://github.com/rust-lang/rust/issues/57640)]
  • Use a prefix operator but allow delaying it [[reference](https://github.com/rust-lang/rust/issues/57640#issuecomment-455782394)]

Stabilizing with a keyword macro await!(fut) is of course future-compatible with basically all of the above, though that does require making the macro use a keyword instead of a regular identifier.

If someone has an example mostly-real-ish example that uses two await in one chain, I'd love to see it; nobody's shared one so far. However, postfix await is also useful even if you don't need to await more than once in a chain, as shown in the reqwest example.

If I missed something notable above this summary comment, PM me on urlo and I'll try to add it in. (Though I will require it to be adding someone else's comments to avoid loud voice favoritism.)

Personally, I've historically been in favor of prefix keyword with the obvious precedence. I still think stabilizing with a keyword macro await!(fut) would be useful to gather real-world information about where the awaiting happens in real world use cases, and would still allow us to add a non-macro prefix or postfix option later.

However, in the process of writing the above summary, I started liking "Keyword Field". "Space Keyword" feels nice when split along multiple lines:

client
    .get("url")
    .send() await?
    .json()?

but on one line, it makes an awkward break that groups the expression poorly: client.get("url").send() await?.json()?. However, with the keyword field, it looks good in both forms: client.get("url").send().await?.json()?

client
    .get("url")
    .send()
    .await?
    .json()?

though I suppose a "keyword method" would flow better, as it is an action. We _could_ even make it a "real" method on Future if we wanted:

trait Future<..> {
    ..
    extern "rust-await" fn r#await(self) -> _;
}

(extern "rust-await" would of course imply all of the magic required to actually do the await and it wouldn't actually be a real fn, it'd mainly just be there because the syntax looks like a method, if a keyword method is used.)

Allow both prefix (obvious precedence) and postfix "field" ...

If any postfix syntax is selected (no matter if together or instead of the prefix one), it would definitely be an argument for a future discussion: we now have keywords that work both in prefix and postfix notation, precisely because sometimes one is preferable over the other, so maybe we could just allow both where it makes sense, and increase the flexibility of the syntax, while unifying the rules. Maybe it's a bad idea, maybe it will be rejected, but it is definitely a discussion to be had in the future, if postix notation is used for await.

I think there's very little chance a syntax that does not include the character string await will be accepted for this syntax.

:+1:


A random thought I had after seeing a bunch of the examples here (such as @mehcode's): One of the complaints I remember about .await is that it's too hard to see†, but given that awaitable things are typically fallible, the fact that it's often .await? helps draw extra attention to it anyway.

† If you're using something that doesn't highlight keywords


@ejmahler

I oppose any syntax that doesn't read somewhat like english

Something like request.get().await reads just as well as something like body.lines().collect(). In "a bunch of chained iterator methods", I think _prefix_ actually reads worse, since you have to remember that they said "wait" way back at the beginning, and never know when you hear something if it's going to be what you're waiting on, kinda like a garden path sentence.

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.

This implies that there are never any inside an expression, which is a restriction I absolutely wouldn't support, given Rust's expression-oriented nature. And at least with C#'s await, it's absolutely plausible to have CallSomething(argument, await whatever.Foo().

Given that async _will_ appear in the middle of expressions, I don't understand why it's easier to see in prefix than it would be in postfix.

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.

return (and continue and break) and while are notable as _completely_ useless to chain, as they always return ! and (). And while for some reason you omitted for, we've seen code written just fine using .for_each() without bad effects, particularly in rayon.

We probably need to make peace with the fact that async/await is going to be a major language feature. It will appear in all kinds of code. It will pervade the ecosystem -- in some places it will be as common as ?. People will have to learn it.

Consequently, we might want to consciously focus on how the syntax choice will feel after using it for a long time rather than how it will feel at first.

We also need to understand that as far as control flow constructs go, await is a different kind of animal. Constructs like return, break, continue, and even yield can be understood intuitively in terms of jmp. When we see these, our eyes bounce across the screen because the control flow we care about is moving elsewhere. However, while await affects the control flow of the machine, it doesn't move the control flow that's important to our eyes and to our intuitive understanding of the code.

We're not tempted to chain unconditional calls to return or break because that would make no sense. For similar reasons, we're not tempted to change our precedence rules to accommodate them. These operators have low precedence. They take everything to the right and return it somewhere, ending execution within that function or block. The await operator, however, wants to be chained. It's an integral part of an expression, not the end of it.

Having considered the discussion and examples in this thread, I'm left with the gnawing sense that we would live to regret surprising precedence rules.

The stalking horse candidate seems to be going with await!(expr) for now and hoping something better is worked out later. Prior to reading @Centril's remarks, I probably would have supported this in the interest of getting this important feature out with nearly any syntax. However, his arguments convince me this would just be copping out. We know that method call chaining is important in Rust. That drove the adoption of the ? keyword, which is widely popular and wildly successful. Using a syntax that we know will disappoint us is indeed just adding technical debt.

Early in this thread, @withoutboats indicated that only four existing options seem viable. Of those, only the postfix expr await syntax is likely to make us happy long-term. This syntax doesn't create strange precedence surprises. It doesn't force us to create a prefix version of the ? operator. It works nicely with method chaining and doesn't break up left-to-right control flow. Our successful ? operator serves as precedent for a postfix operator, and await is more like ? in practice than it is like return, break, or yield. While a postfix non-symbol operator may be new in Rust, usage of async/await will be widespread enough to make it quickly familiar.

While all of the options for a postfix syntax seem workable, expr await has some advantages. This syntax makes it clear that await is a keyword, which helps to emphasize the magic control flow. Compared with expr.await, expr.await() expr.await!, expr.await!(), etc., this avoids having to explain that this looks like a field/method/macro, but really isn't in this one special case. We would all get used to the space separator here.

Spelling await as @ or using some other symbol that doesn't cause parsing problems is appealing. It's certainly an important enough operator to warrant it. But if, in the end, it has to be spelled await, that will be fine. As long as it's in postfix position.

As someone mentioned _real_ examples ... I maintain a (according to tokei) 23,858 line rust codebase that is very heavily async and uses futures 0.1 await (highly experimental I know). Let's go (redacted) spelunking (note everything has been run through rustfmt):

// A
if !await!(db.is_trusted_identity(recipient.clone(), message.key.clone()))? {
    info!("recipient: {}", recipient);
}

// B
match await!(db.load(message.key))? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await!(client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())?
.error_for_status()?;

// D
let mut res =
    await!(client.get(inbox_url).headers(inbox_headers).send())?.error_for_status()?;

let mut res: InboxResponse = await!(res.json())?;

// E
let mut res = await!(client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())?
.error_for_status()?;

let res: Response = await!(res.json())?;

// F
#[async]
fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await!(self.request(url, Method::GET, None, true))?;
    let user = await!(res.json::<UserResponse>())?
        .user
        .into();

    Ok(user)
}

Now lets transform this into the most popular prefix variant, obvious precedence with sugar. For obvious reasons this has not been run through rustfmt so apologies if there is a better way to write it.

// A
if await? db.is_trusted_identity(recipient.clone(), message.key.clone()) {
    info!("recipient: {}", recipient);
}

// B
match await? db.load(message.key) {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = (await? client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())
.error_for_status()?;

// D
let mut res =
    (await? client.get(inbox_url).headers(inbox_headers).send()).error_for_status()?;

let mut res: InboxResponse = await? res.json();

// E
let mut res = (await? client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())
.error_for_status()?;

let res: Response = await? res.json();

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await? self.request(url, Method::GET, None, true);
    let user = (await? res.json::<UserResponse>())
        .user
        .into();

    Ok(user)
}

Finally, lets transform this into my favorite postfix variant, "postfix field".

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()).await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key).await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send().await?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send().await?
    .error_for_status()?
    .json().await?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json().await?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true).await?
        .res.json::<UserResponse>().await?
        .user
        .into();

    Ok(user)
}

After this exercise, I find a number of things.

  • I now am strongly against await? foo. It reads nice for simple expressions but the ? feels lost on complex expressions. If we must do prefix I'd rather have "useful" precedence.

  • Using postfix notation leads me to join statements and reduce unnecessary let bindings.

  • Using postfix _field_ notation leads me to strongly prefer .await? to appear in the line of the thing its await rather than on its own line in rustfmt parlance.

I appreciate the ellipses postfix notation "..." above , both for its conciseness and symbolically in English language representing a pause in anticipation of something else. (Like how async behavior works!), it also chains together well.

let resultValue = doSomethingAndReturnResult()...?;
let resultValue = doSomethingAndReturnResult()...?.doSomethingOnResult()...?;
let value = doSomethingAndReturnValue()....doSomethingOnValue()...;
let arrayOfValues = vec![doSomethingA(),doSomethingB()]...?;
// Showing stacking
let value = doSomethingWithVeryLongFunctionName()...?
                 .doSomethingWithResult()...?;

I doubt any other options will be as concise and visually meaningful.

A

let mut res: Response = (await client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json())?;

B

let mut res: Response = await client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json());
let res = res.unwrap();

Should it even be considered good form to have long chains of awaits?

Why not simply use regular Future combinators?

In fact, some expressions don't translate well into await chains if you want to have backup behaviors on failure.

Personally I think this:

let value = await some_op()
                 .and_then(|v| v.another_op())
                 .and_then(|v2| v2.final_op())
                 .or_else(|| backup_op());

value.unwrap()

reads far more naturally than this:

let value = match await some_op() {
    Ok(v) => match await v.another_op() {
        Ok(v2) => await v2.final_op(),
        Err(_) => await backup_op(),
    },
    Err(_) => await backup_op(),
};

value.unwrap()

We still have the full power of zero-cost futures at our hands, after all.

Consider that.

@EyeOfPython I would like to hightlight that we have another choice than @ in future@await. We can write future~await, where the ~ is worked like a semi hyphen, and would work for any possible postfix operators.

The hyphen - was already used as the minus operator and negative operator. No longer good. But ~ was used to indicate heap objects in Rust, and otherwise it was hardly used in any programming languages. It sould gives less confusions for people from other languages.

@earthengine Good idea, but maybe just use future~ which means await a future, where the ~ is worked like the await keyword.(like ? symbol

Future | Future of Result | Result of Future
-- | -- | --
future~|future~?|future?~

Also chained futures like:

let res: MyResponse = client.get("https://my_api").send()~?.json()~?;

I've take a look at how Go implements async programming and found something interesting there. The closest alternative to futures in Go are channels. And instead of await or other screaming syntax to wait for values Go channels just provides <- operator for that purpose. For me it looks pretty clean and straightforward. Previously I've often seen how people praises Go for its simple syntax and good async facilities, so it's definitely a good idea to learn something from its experience.

Unfortunatelly, we couldn't have exactly the same syntax because there's a lot more angle braces in Rust source code than in Go, mostly because of generics. This makes <- operator really subtle and not pleasant to work with. Another disadvantage is that it could be seen as opposite to -> in function signature and there's no reason to consider it like that. And yet another disadvantage is that <- sigil was intended to be implemented as placement new, so people could misinterpret it.

So, after some experiments with syntax I stopped at <-- sigil:

let output = <-- future;

In async context <-- is pretty straightforward, although less than <-. But instead it provides a big advantage over <- as well as over prefix await - it plays well with indentation.

async fn log_service(&self) -> T {
   let service = self.myService.foo();
   <-- self.logger.log("beginning service call");
   let output = <-- service.exec();
   <-- self.logger.log("foo executed with result {}.", output));
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = <-- acquire_lock();
    let length = <-- logger.log_into(message)?;
    <-- logger.timestamp();
    Ok(length)
}

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

For me this operator looks even more unique and easier to spot than await (which looks more like any other keyword or variable in code context). Unfortunately, on Github it looks more thin than in my code editor but I think that's not fatal and we can live with that. Even if someone feels uncomfortable, different syntax highlighting or better font (especially with ligatures) will resolve all of the problems.

Another reason to like this syntax is because it conceptually could be expressed as "something that's not yet here". Right to left direction of arrow is opposite to the direction how we read text, which allows us to describe it as "thing that comes from future". The long shape of <-- operator also suggests that "some durable operation begins". Angle bracket and two hyphens directed from future could symbolize "continuous polling". And we still able to read it as "await" like it was before.
Not some kind of enlightenment, but might be fun.


The most important thing in this proposal is that ergonomic method chaining also would be possible. The idea of delayed prefix operator which I've proposed previously is a good fit here. In this way we would have the best from both worlds of prefix and postfix await syntax. I really hope that there also would be introduced some useful extras which personally I wanted in many occasions before: delayed dereferencing and delayed negation syntax.

Through, I'm not sure if delayed word is proper here, maybe we should name it differently.

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

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

Operator precedence looks pretty obvious: from left to right.

I hope this syntax will reduce need in writing is_not_* functions which purpose is only to negate and return a bool property. And boolean/dereferencing expressions in some cases will be more cleaner when using it.


Finally, I've applied it on real world examples posted by @mehcode and I like how <-- makes proper emphasis on async function inside of method call chains. Contrarily, postfix await just looks like regular field access or function call (depending on syntax) and it's nearly impossible to distinguish them between without special syntax highlighting or formatting.

// A
if db.<--is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.<--load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .<--send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .<--send()?
    .error_for_status()?
    .<--json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .<--send()?
    .error_for_status()?
    .<--json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.<--request(url, Method::GET, None, true)?
        .res.<--json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

After all: that's the syntax I want to use.

@novacrazy

Should it even be considered good form to have long chains of awaits? Why not simply use regular Future combinators?

I'm not sure if I didn't misunderstand you but those are not exclusive, you can still use combinators as well

let value = some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())
    .or_else(|| backup_op())
    .await;

But for this to work, the and_then clause needs to be typed at FnOnce(T) -> impl Future<_>, not only at FnOnce(T) -> U. Doing chained combinators on the future and the result only works cleanly without paranthesis in postfix:

let result = load_local_file()
    .or_else(|_| request_from_server()) // Async combinator
    .await
    .and_then(|body| serde_json::from_str(&body)); // Sync combinator

In this post I'll focus on the question of operator precedence in the post-fix case. As far as I can tell, we have three viable alternatives that at least work to some extent, each exemplified by one post-fix expression that has these precedences in current syntax. I feel strongly that none of the proposals should change current operator precedence.

  • Method call (future.await())
  • Field expression (future.await)
  • Function call (future(await))

The differences in functionality are rather small but existent. Note that I would accept all of these, this is mostly finetuning. To show them all, we need some types. Please don't comment on the contrivedness of the example, this is the most compressed version that shows all differences at once.

struct Foo<A, F, S> where A: Future<Output=F>, F: FnOnce(usize) -> S {
    member: A,
}

// What we want to do, in macro syntax:
let foo: Foo<_, _, _> = …;
(await!(foo.member))(42)
  • Method call: foo.member.await()(42)
    Binds most strongly, so no paranthesis at all
  • Member: (foo.member.await)(42)
    Needs paranthesis around the awaited result when this is a callable, this is consistent with having a callable as a member, otherwise confusion with call to member function. This also suggests like one could destructure with patterns: let … { await: value } = foo.member; value(42) somehow?
  • Function call: (foo.member)(await)(42)
    Needs paranthesis for destructuring (we move member) as it behaves as function call.

All of them look the same when we neither destructure an input struct through move of a member, nor call the result as a callable since these three precedence classes come directly after one another. How do we want futures to behave?

The best parallel to method call should be just another method call taking self.

The best parallel to member is a struct that only has the implicit member await and thus is destructured by moving from this, and this move implicitely awaits the future. This one feels like the least obivous.

The parallel to function is call is the behaviour of closures. I'd prefer this solution as the cleanest addition to the language corpus (as in syntax best parallels the possibilities of the type) but are some positive points for method call and .await is never longer than the others.

@HeroicKatora We could special case .await in libsyntax to allow for foo.await(42) but this would be inconsistent / ad-hoc. However, (foo.await)(42) seems serviceable since while futures outputting closures exist, they are probably not all that common. Thus if we optimize for the common case, not having to add () to .await likely wins out on balance.

@Centril I agree, consistency is important. When I see a couple of behaviours that are similar, I'd like to infer others through synthesis. But here .await seems awkward, especially with the above example clearly showing that it parallels implicit destructuring (unless you can find a different syntax where these effects occur?). When I see destructuring, I instantly wonder whether I can use this with let-bindings etc. This, however, would not be possible because we'd destructure the original type that has no such member or especially when the type is just impl Future<Output=F> (Mostly irrelevant sidenote: making this work would brings us back to an alternative prefix await _ = in the place of let _ =, funnily¹).

That doesn't forbid us from using the syntax per-se, I think I could deal with learning it and if it turns out to be the final one I will use it with vigor, but it seems like a clear weakness.


¹ This could be consistent while allowing ? by permitting ? behind names in a pattern

  • await value? = failing_future();

to match the Ok part of a Result. This seems interesting to explore in other contexts as well but rather off-topic. It would also lead to matching prefix and suffix syntax for await at the same time.

That doesn't forbid us from using the syntax per-se, I think I could deal with learning it and if it turns out to be the final one I will use it with vigor, but it seems like a clear weakness.

I think every solution will have some drawback in some dimension or case re. consistency, ergonomics, chainability, readability, ... This makes it a question of degree, importance of the cases, fitness to typical Rust code and APIs, etc.

In the case of a user writing foo.await(42)...

struct HasClosure<F: FnOnce(u8)> { closure: F, }
fn _foo() {
    let foo: HasClosure<_> = HasClosure { closure: |x| {} };

    foo.closure(42);
}

...we already provide good diagnostics:

5 |     foo.closure(42);
  |         ^^^^^^^ field, not a method
  |
  = help: use `(foo.closure)(...)` if you meant to call the function stored in the
          `closure` field

Tweaking this to fit foo.await(42) seems quite attainable. In fact, as far as I can see, we know that the user intends (foo.await)(42) when foo.await(42) is written so this can be cargo fixed in a MachineApplicable manner. Indeed, if we stabilize foo.await but don't allow foo.await(42) I believe we can even change the precedence later if we need to since foo.await(42) won't be legal at first.

Further nestings would work (e.g. future of result of closure -- not that this will be common):
```rust
struct HasClosure fn _foo() -> Result<(), ()> {
let foo: HasClosure<_> = HasClosure { closure: Ok(|x| {}) };

foo.closure?(42);

Ok(())

}

e.g. future of result of closure -- not that this will be common

The extra suffix ? operator makes this unambiguous without modifications to syntax–in any of the post-fix examples. No tweaking necessary. The problems are only to .member explicitely being a field and the need for move-destructuring to apply first. And I really don't want to say that this would be hard to write. I mostly want to say that this seem inconsistent with other .member uses which e.g. can be transformed to matching. The original post was weighing positives and negatives in that regard.

Edit: Tweaking to fit future.await(42) has the, likely unintended, extra risk of making this a) inconsistent with closures where this is not the case due to methods of the same name as member being allowed; b) inhibiting future developments where we'd like to give arguments to await. But, as you previously mentioned, tweaking for Future returning a closure should not be the most pressing issue.

@novacrazy Why not simply use regular Future combinators?

I'm not sure how much experience you have with Futures 0.3, but the general expectation is that combinators won't be used much, and the primary/idiomatic usage will be async/await.

Async/await has several advantages over combinators, e.g. it supports borrowing across yield points.

Combinators existed long before async/await, but async/await was invented anyways, and for good reason!

Async/await is here to stay, which means that it needs to be ergonomic (including with method chains).

Of course people are free to use the combinators if they wish, but they shouldn't be necessary in order to get good ergonomics.

As @cramertj said, let's try to keep the discussion focused on async/await, not alternatives to async/await.

In fact, some expressions don't translate well into await chains if you want to have backup behaviors on failure.

Your example can be simplified significantly:

let value = try {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => await backup_op(),
}.unwrap()

This makes it clear which parts are handling the error, and which parts are on the normal happy path.

This is one of the great things about async/await: it works well with other parts of the language, including loops, branches, match, ?, try, etc.

In fact, aside from await, this is the same code you would write if you weren't using Futures.

Another way to write it, if you prefer using the or_else combinator:

let value = await async {
    try {
        let v = await some_op()?;
        let v2 = await v.another_op()?;
        await v2.final_op()?
    }
}.or_else(|_| backup_op());

value.unwrap()

And best of all is to move the normal code into a separate function, making the error handling code even clearer:

async fn doit() -> Result<Foo, Bar> {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()
}
let value = await doit().or_else(|_| backup_op());

value.unwrap()

(This is a reply to @joshtriplett's comment).

To be clear, you do not have to parenthesize, I mentioned it because some people said that it's too hard to read without the parentheses. So the parentheses are an optional stylistic choice (useful only for complex one-liners).

All of the syntaxes benefit from parentheses in some situations, none of the syntaxes are perfect, it's a question of which situations we want to optimize for.

Also, after re-reading your comment, perhaps you thought I was advocating for prefix await? I wasn't, my example was using postfix await. I overall like postfix await, though I like some of the other syntaxes too.

I'm starting to get comfortable with fut.await, I think people's initial reaction will be "Wait, that's how you do await? Weird." but later they'd love it for the convenience. Of course, the same is true for @await, which stands out much more than .await.

With that syntax, we can leave out some of the lets in the example:

`.await` `@await`
let value = try {
    some_op().await?
        .another_op().await?
        .final_op().await?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => backup_op().await,
}.unwrap()
let value = try {
    some_op()@await?
        .another_op()@await?
        .final_op()@await?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => backup_op()@await,
}.unwrap()

This also makes clearer what's getting unwrapped with ?, for await some_op()?, it's not obvious whether some_op() gets unwrapped or the awaited result.

@Pauan

I’m not trying to shift focus away from the topic here, I’m trying to point out it doesn’t exist in a bubble. We have to consider how things work together.

Even if the ideal syntax was chosen, I’d still want to use custom futures and combinators in some situations. The idea that those could be soft deprecated makes me question the entire direction of Rust.

The examples you give still look terrible compared to the combinators, and with the generator overhead will probably be a bit slower and produce more machine code.

As far as await goes, all this prefix/postfix sigil/keyword bikeshedding is wonderful, but perhaps we should be pragmatic and go with the simplest option that is most familiar to users coming to Rust. I.e.: prefix keyword

This year is going to go by faster than we think. Even January is mostly done. If it turns out that users aren’t happy with a prefix keyword, it can be changed in a 2019/2020 edition. We can even make a “hindsight is 2020” joke.

@novacrazy

General consensus I've seen is that the _earliest_ we'd want a third edition is 2022. We definitely don't want to plan around another edition; the 2018 edition was great but not without its costs. (And one of the points of the 2018 edition is making async/await possible, imagine taking that back and saying "nope, you need to upgrade to the 2020 edition now!")

In any case, I don't think that a prefix keyword -> postfix keyword transition is possible in an edition, even if it would be desirable. The rule around editions is that there needs to be a way to write idiomatic code in edition X such that it compiles without warnings and works the same in edition X+1.

It's the same reason that we'd rather not stabilize with a keyword macro if we can drive consensus on a different solution; deliberately stabilizing a solution we know is undesirable is itself problematic.

I think we've shown that a postfix solution is more optimal, even for expressions with only one await point. But I doubt that any of the proposed postfix solutions is obviously better than all the others.

Just my two cents (I am a nobody, but I follow the discussion for quite a long time). My favorite solution would be the @await postfix version. Maybe you could consider a postfix !await, like some new postfix macro syntax?

Example:

let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()!await?
    .error_for_status()?
    .json()!await?;

After a few language iterations, being able to implement our own postfix macros would be awesome.

... all new sygils proposals

Rust is already syntax/sygil heavy, we have await keyword reserved, stuff@await (or any other sygil) looks weird/ugly (subjective, I know), and is just ad-hoc syntax that does not integrate with anything else in the language which is a big red flag.

I've take a look at how Go implements
... <-- ... proposal

@I60R : Go has a terrible syntax full of ad-hoc solutions, and is totally imperative, very much unlike Rust. This proposal is again sygil/syntax heavy and totally ad-hoc just for this particular feature.

@I60R : Go has a terrible syntax

Let's please refrain from bashing other languages here. "X has terrible syntax" does not lead to enlightenment and to consensus.

As a Python/JavaScript/Rust user and a computer science student, I personally prefer prefix await + f.await() to be both in the language.

  1. Both Python and JavaScript have prefix await. I would expect to see await appear in the beginning. If I have to read deep into line to realize this is asynchronous code, I feel very uneasy. With Rust's WASM capability, it might attract many JS developers. I believe familiarity and comfort is really important, considering Rust already has a lot of other new concepts.

  2. Postfix await seems convenient in chaining settings. However, I dislike solutions like .await, @await, f await because they look like ad-hoc solution to await syntax while it makes sense to think .await() as calling a method on the future.

Rust is already a departure from javascript and await is nothing like calling function(i.e functionality cant be emulated via a function) using functions to denote await makes it confusing to first timers being introduced to async-await. Hence i think the syntax should be different.

I've convinced myself that .await() is probably significantly more desirable over .await, though the rest of this post hedges that position a bit.

The reason for this is that await!(fut) has to _consume fut by value_. Making it look like a field access is _bad_, because that doesn't have the connotation of moving the way that a prefix keyword does, or the potential of moving like a macro or a method call does.

Interestingly, the keyword method syntax makes it almost look like an implicit await design. Unfortunately, "Explicit Async, Implicit Await" isn't possible for Rust (so _please_ don't re-litigate it on this thread) since we want async fn() -> T to be used identically to fn() -> Future<T>, rather than activate an "implicit await" behavior.

The fact that a .await() syntax _looks_ like an implicit await system (like Kotlin uses) could be a detractor, and would almost give the feel of an "Implicit Async, Implicit Await" due to the magic around the not-really-a-method-call .await() syntax. Would you be able to use that as await(fut) with UFCS? Would it be Future::await(fut) for UFCS? Any syntax that looks like another dimension of the language raises problems unless it can be somewhat unified with it at least syntactically, even if not functionally.

I remain skeptical if the benefits of any _individual_ postfix solution outweigh the drawbacks of that same solution, though the concept of a postfix solution is more desirable than a prefix one generally.

I'm a bit surprised that this thread is full of suggestions that seem to be made because they are possible - and not because they seem to yield any significant benefits over the initial suggestions.
Can we stop talking about $, #, @, !, ~ etc, without bringing up a significant argument what is wrong with await, which is well understand and has proven itself in various other programming languages?

I think the post from https://github.com/rust-lang/rust/issues/57640#issuecomment-455361619 already listed all good options.

From those:

  • Mandatory delimiters delimiters seem to be fine, at least it's obvious what the precedence is and we can read others code without more clarity. And typing two parentheses isn't a that bad. Maybe the only downside is that it looks like a function call, even though it's some different control flow operation.
  • Useful precedence might be the preferable option. That seems to be the road that most other languages went, so it is familiar and proven.
  • I personally think postfix keyword with whitespace looks weirds with multiple awaits in a single statement:
    client.get("url").send() await?.json()?. That whitespace in between looks out of place. With parentheses it would make a little more sense for me: (client.get("url").send() await)?.json()?
    But I find the control flow still harder to follow than with the prefix variant.
  • I don't like postfix field. await does a very complex operation - Rust has no computable properties and field access is otherwise a very simple operation. So it seems to give a wrong impression about the complexity of this operation.
  • Postfix method could be OK. Might encourage some people to write very long statements with multiple awaits into them, which might hide the yield points more. It also again makes things look like a method call, even though it's something different.

For these reasons I prefer the "useful precedence" followed by "mandatory delimiters"

Go has a terrible syntax full of ad-hoc solutions, and is totally imperative, very much unlike Rust. This proposal is again sygil/syntax heavy and totally ad-hoc just for this particular feature.

@dpc, if you read <-- proposal completely, you would see that this syntax is only inspired by Go, however is pretty different and usable either in imperative and in function chaining context. I also fail to see how await syntax isn't ad-hoc solution, for me it's way more specific and way more clumsy than <--. It's similar to have deref reference/reference.deref/etc instead of *reference, or have try result/result.try/etc instead of result?. I either don't see any advantage of using await keyword other than familiarity with JS/Python/etc which anyway should be less significant than having consistent and composable syntax. And I don't see any disadvantage of having <-- sigil other than it's not an await which anyway isn't that simple as plain English and users should understand what it does first.

Edit: this as well could be a good answer to @Matthias247 post, since it provides some arguments against await and proposes a possible alternative not affected with the same problems


It's really interesting for me, through, to read critique against <-- syntax, free from arguments that appeals to historical and prejudiced reasons.

Let's mention actual specifics around precedence:

The precedence chart as it stands today:

Operator/Expression | Associativity
-- | --
Paths |  
Method calls |  
Field expressions | left to right
Function calls, array indexing |  
? |  
Unary - * ! & &mut |  
as | left to right
* / % | left to right
+ - | left to right
<< >> | left to right
& | left to right
^ | left to right
\| | left to right
== != < > <= >= | Require parentheses
&& | left to right
\|\| | left to right
.. ..= | Require parentheses
= += -= *= /= %= &= \|= ^= <<= >>= | right to left
return break closures |  

Useful precedence puts await before ? so that it binds more tightly than ?. A ? in the chain thus binds an `await to everything before it.

let res = await client
    .get("url")
    .send()?
    .json();

Yes, with useful precedence, that "just works". Do you know what that does at a glance? Is that bad style (probably)? If so, can rustfmt fix that automatically?

Obvious precedence puts await _somewhere_ below ?. I'm not sure exactly where, though those specifics probably don't matter too much.

let res = await? (client
    .get("url")
    .send())
    .json();

Do you know what that does at a glance? Can rustfmt put it into a useful style that isn't too wasteful of vertical and horizontal space automatically?


Where would postfix keywords fall in this? Probably Keyword Method with Method calls and Keyword Field with Field expressions, but I'm unsure how the others should bind. What options lead to the least possible configurations where the await receives a surprising "argument"?

For this comparison, I suspect "mandatory delimiters" (what I called Keyword Function in the summary) wins easily, as it would be equivalent to a normal function call.

@CAD97 To be clear, remember that .json() is also a future (at least in reqwests).

let res = await await client
    .get("url")
    .send()?
    .json()?;
let res = await? await? (client
    .get("url")
    .send())
    .json();

The more I play with converting complex rust expressions (even ones with only need for 1 await, however, note that in my 20,000+ future code base almost every single async expression is an await, directly followed by another await), the more I dislike prefix for Rust.

This is _all_ because of the ? operator. No other language has a postfix control flow operator _and_ await that are essentially always paired in real code.


My preference is still postfix field. As a postfix control operator, I feel it needs the tight visual grouping that future.await provides over future await. And comparing to .await()?, I prefer how .await? looks _weird_ so it will be _noticed_ and users won't assume it's a simple function (and thus won't ask why UFCS doesn't work).


As one more data point in favor of postfix, when this is stabilized, a rustfix to go from await!(...) to whatever we decide would be very much appreciated. I don't see how anything but the postfix syntax could be unambiguously translated without wrapping stuff in ( ... ) unnecessarily.

I think first we should answer the question "do we want to encourage await usage in chaining contexts?". I believe the prevalent answer is "yes", so it becomes a strong argument for postfix variants. While await!(..) will be the easiest to add, I believe we should not repeat the try!(..) story. Also I personally disagree with the argument that "chaining hides potentially costly operation", we already have a lot of chaining methods which can be _very_ heavy, so chaining does not entail lazyness.

While the prefix await keyword will be the most familiar for users coming from other languages, I don't think we should make our decision based on it and instead we should concentrate on longer term, i.e. usability, convenience and readability. @withoutboats talked about "familiarity budget", but I strongly believe we should not introduce sub-optimal solutions just for familiarity sake.

Now we probably don't want two ways of doing the same thing, so we should not introduce both postfix and prefix variants. So let's say we've narrowed our options to postfix variants.

First let's start with fut await, I strongly dislike this variant, because it will seriously mess with how humans parse code and it will be a constant source of confusion while reading code. (Don't forget that code is mostly for reading)

Next fut.await, fut.await() and fut.await!(). I think the most consistent and less confusing variant will be the postfix macro one. I don't think it's worth to introduce a new "keyword function" or "keyword method" entity just to save a couple of characters.

Lastly sigil based variants: fut@await and fut@. I don't like the fut@await variant, if we introduce the sigil why bother with the await part? Do we have plans for future extensions fut@something? If not, it simply feels redundant. So I like the fut@ variant, it solves the precedence issues, code becomes easy to understand, write and read. Visibility issues can be solved by code highlighting. It will not be hard to explain this feature as "@ for await". Of course the biggest drawback is that we will pay for this feature from the very limited "sigil budget", but considering the importance of the feature and how often it will be used in async codebases, I believe it will be worth in a long run. And of course we can draw certain parallels with ?. (Though we will have to be prepared for Perl jokes from Rust critics)

In conclusion: in my opinion if we are ready to burden "sigil budget" we should go with fut@, and if not with fut.await!().

When talking about familiarity, I don't think that we should care too much about familiarity with JS/Python/C#, since Rust is in different niche and already looks differently in many things. Providing syntax similar to these languages is short term and low reward goal. Nobody will select Rust only for using familiar keyword when under the hood it works completely different.

But familiarity with Go matter, since Rust is in similar niche and even by philosophy it's more close to Go than to other languages. And despite all of prejudiced hate, one of the strongest points in them both is that they don't blindly copy features, but instead implements solutions that really have reason for.

IMO, in this sense <-- syntax is strongest here

With all that, let's remember that Rust expressions can result in several chained methods. Most languages tend to not do that.

I'd like to remind C# dev team experience:

The main consideration against C# syntax is operator precedence await foo?

This is something i do feel like i can comment on. We thought about precedence a lot with 'await' and we tried out many forms before setting on the form we wanted. One of the core things we found was that for us, and the customers (internal and external) that wanted to use this feature, it was rarely the case that people really wanted to 'chain' anything past their async call. In other words, people seemed to strongly gravitate toward 'await' being the most important part of any full-expression, and thus having it be near the top. Note: by 'full expression' i mean things like the expression you get at the top of a expression-statement, or hte expression on the right of a top level assign, or the expression you pass as an 'argument' to something.

The tendency for people to want to 'continue on' with the 'await' inside an expr was rare. We do occasionally see things like (await expr).M(), but those seem less common and less desirable than the amount of people doing await expr.M().

and

This is also why we didn't go with any 'implicit' form for 'await'. In practice it was something people wanted to think very clearly about, and which they wanted front-and-center in their code so they could pay attention to it. Interestingly enough, even years later, this tendency has remained. i.e. sometimes we regret many years later that something is excessively verbose. Some features are good in that way early on, but once people are comfortable with it, are better suited with something terser. That has not been the case with 'await'. People still seem to really like the heavy-weight nature of that keyword and the precedence we picked.

Is a good point against sigil instead of dedicated (key)word.

https://github.com/rust-lang/rust/issues/50547#issuecomment-388939886

You should really listen to the guys with millions of users.

So you don't want to chain anything, you just want to have several await's, and my experience is the same. Writing async/await code for more than 6 years, and I never wanted such a feature. Postfix syntax looks really alien and is considered to resolve a situation that is likely to never happen. Async call is really a bold thing so several awaits on single line is too heavy.

The tendency for people to want to 'continue on' with the 'await' inside an expr was rare. We do occasionally see things like (await expr).M(), but those seem less common and less desirable than the amount of people doing await expr.M().

That seems like a-posteriori analysis. Maybe one of the reasons why they don't continue is because it is extremely awkward to do so in prefix syntax (Comparable to not wanting to try! multiple times in a statement because that remains readable through operator ?). The above mostly considers (as far as I can tell) precendence, not position. And I would like to remind you that C# is not Rust, and trait members may change quite a bit the desire to call methods on results.

@I60R,

  1. I think familiarity is important. Rust is relatively new language and people will be migrating from other languages and if Rust will be looking familiar it will be easier for them to make a decision to choose the Rust.
  2. I am not big fun of chaining methods - it is much harder to debug long chains and I thing chaining is just complicating code readability and may be allowed only as additional option (like as macros .await!()). The prefix form will force developers to extract code into methods instead of chaining, like:
let resp = await client.get("http://api")?;
let body: MyResponse = await resp.into_json()?;

into something like this:

let body: MyResponse = await client.get_json("http://api")?;

That seems like a-posteriori analysis. Maybe one of the reasons why they don't continue is because it is extremely awkward to do so in prefix syntax. The above only considers precendence, not position. And I would like to remind you that C# is not Rust, and trait members may change quite a bit the desire to call methods on results.

No, it's about internal C# team experiments when it had both prefix/postfix/implicit forms. And I'm talking about my experience which is not just a habit where I'm unable to see postfix form pros.

@mehcode Your example doesn't motivate me. reqwest consciously decides to make the initial request/response cycle and the subsequent handling of the response (body stream) seperate concurrent processes, hence they should be awaited twice, like @andreytkachenko shows.

reqwest could totally expose the following APIs:

let res = await client
    .get("url")
    .json()
    .send();

or

let res = await client
    .get("url")
    .send()
    .json();

(The latter being a simple sugar over and_then).

I find it troubling that a lot of the postfix examples here use this chain as an example, as reqwests and hypers best api decision is keeping these things separate.

I honestly believe that most postfix await examples here should be rewritten using combinators (and sugar if necessary) or be kept similar operations and awaited multiple times.

@andreytkachenko

The prefix form will force developers to extract code into methods instead of chaining, like:

Is that a good thing or a bad thing? For cases where there's N methods, each that can result in M follow-ups, developer is supposed to provide N * M methods? I personally, like composable solutions, even if they are a bit longer.

into something like this:

let body: MyResponse = await client.get_json("http://api")?;
let body: MyResponse = client.get("http://api").await?.into_json().await?;

@Pzixel

I don't think in C# you'd get as much chained/functional code as you would in Rust. Or am I wrong? That makes experiences of C# devs/users interesting, but not necessarily applicable for Rust. I wish we could contrast with Ocaml or Haskell.

I think that's the root cause of the disagreement is that some of us enjoy imperative style, and some functional one. And that's it. Rust supports both, and both sides of the debate want async to fit well in the way they typically write code.

@dpc

I don't think in C# you'd get as much chained/functional code as you would in Rust. Or am I wrong? That makes experiences of C# devs/users interesting, but not necessarily applicable for Rust. I wish we could contrast with Ocaml or Haskell.

LINQ and functional style are pretty popular in C#.

@dpc,
if you are about readability - I thing that as much code explicit - as better, and my experience says that if methods/functions names are well self-describing it is not necessary to do the follow-ups.

if you about overhead - Rust compiler is rather smart to inline them (anyway we always have #[inline]).

@dpc

let body: MyResponse = client.get("http://api").await?.into_json().await?;

My feeling is that this basically repeats the problem of the futures API: it makes chaining easier, but the type of that chain becomes much harder to penetrate and debug.

Doesn't that same argument apply to the ? operator though? It allows for more chaining and thus makes debugging harder. But why did we choose ? over try! then? And why does Rust prefer APIs with builder pattern? So Rust already made a bunch of choices favoring chaining in APIs. And yes, that may make debugging harder, but shouldn't that happen on a lint level - maybe a new lint for clippy for chains that are too large? I still have to see sufficient motivation as to how await is different here.

Is that a good thing or a bad thing? For cases where there's N methods, each that can result in M follow-ups, developer is supposed to provide N * M methods? I personally, like composable solutions, even if they are a bit longer.

N and M are not necessarily large, and also not all of them might be of interest/useful to extract.

@andreytkachenko,

  1. I don't agree, familiarity is overrated here. When migrating from other language first people would seek ability to do async-await-style programming but not for exactly the same syntax. If it would be implemented differently but that would provide better programming experience, then it will become as yet another advantage.

  2. Inability to debug long chains is limitation of debuggers not of code style. About readability I think it depends, and enforcing imperative style is unnecesarely restrictive here. If async function calls are still function calls then it's surprising to not support chaining. And anyway, <-- is a prefix operator with ability to use it in function chains as additional option, as you said

@skade Agreed.

All the examples with long chains of awaits are a code smell. They could be solved far more elegantly with combinators or updated APIs, rather than creating these generator spaghetti state machines under the hood. Extreme shorthand syntax is a recipe for even more difficult debugging than current futures.

I'm still a fan of both prefix keyword await and macro await!(...)/.await!(), in combination with async and #[async] generator functions, as described in my comment Here.

If everything goes correctly, a single macro could probably be created for await! to handle both prefix and suffix, and the await keyword would only be a keyword inside of async functions.

@skade,
For me a large disadvantage of using combinators on future is that they will hide async function calls and it would be impossible to distinguish them from regular functions. I want to see all suspendable points as it's very likely that they would be used inside of builder-style chains.

@I60R debugging long call chains is totally not a debugger problem, because the problem with writing these chains is getting type inference right. Given that many of those methods take generic parameters and hand out generic parameters, potentially bound to a closure, is severe problem.

I'm not sure if making all suspension points visible is a goal of the feature. This is on the implementers to decide. And it's totally possible with all proposed versions of the await syntax.

@novacrazy good point, now that you mention it, as a former javascripter, I NEVER use await chains, I always used then blocks

I guess it would look something like

let result = (await doSomethingAsync()
          .then(|result| {
                     match result {
                          Ok(v) => doSomethingAsyncWithFirstResponse(v)
                          Err(e) => Future.Resolve(Err(e))
                      }
            }).then(|result| {
                  Ok(result.unwrap())
            })).unwrap();

which I don't even know if its possible, I need to look up on futures in Rust

@richardanaya this is totally possible (different syntax).

Like the Box impl:

impl<T> Box<T> {
    #[inline]
    pub fn new(x: T) -> Box<T> {
        box x
    }
    ...
}

We can enter a new keyword await and trait Await with like this impl:

impl<T> Await for T {
    #[inline]
    pub fn await(self) -> T {
        await self
    }
    ...
}

And use await as a method and as a keyword too:

let result = await foo();
first().await()?.second().await()?;

@richardanaya Agreed. I was among some of the first devs to adopt async/await for my webdev stuff many years ago, and the real power came from combining async/await with the existing Promises/Futures. Also, even your example could probably be simplified as:

let result = await doSomethingAsync()
                  .and_then(doSomethingAsyncWithFirstResponse);

let value = result.unwrap();

If you have nested futures or results of futures or futures of results, the .flatten() combinator can further simplify that drastically. It would be bad form to unwrap and await on every single one manually.

@XX That is redundant and invalid. await only exists in async functions, and can only work on types that implement Future/IntoFuture anyway, so there isn't a need for a new trait.

@novacrazy , @richardanaya

they could be solved far more elegantly with combinators or updated APIs, rather than creating these generator spaghetti state machines under the hood. Extreme shorthand syntax is a recipe for even more difficult debugging than current futures.

@novacrazy good point, now that you mention it, as a former javascripter, I NEVER use await chains, I always used then blocks

Combinators have totally different properties and powers in Rusts async/await, e.g. regarding borrowing across yield points. You can not safely write combinators that have the same abilities as async blocks. Let's leave the combinator discussion out of this thread, since it's not helpful.

@I60R

For me a large disadvantage of using combinators on future is that they will hide async function calls and it would be impossible to distinguish them from regular functions. I want to see all suspendable points as it's very likely that they would be used inside of builder-style chains.

You can not hide suspend points. The only thing that combinators allow is creating other futures. At some point of time those must be awaited.

Keep in mind that C# doesn't have the issue where most code using a prefix await will combine it with the (already-existing) postfix ? operator. In particular, questions about precedence, and the general awkwardness of having two similar "decorators" appear on opposite sides of an expression.

@Matthias247 If you do need to borrow data, sure, feel free to use multiple await statements. However, often times you simply need to move data, and combinators are perfectly valid for that, and have the potential of compiling down to more efficient code. Sometimes being optimized away completely.

Again, the real power is in combining things together. There is no one right way of doing things as complicated as this. That's partly why I consider all this syntax bikeshedding to be exactly that if it's not going to help new users coming from other language and help create maintainable, performant and readable code. 80% of the time I doubt I'll even touch async/await no matter what the syntax is, just to provide the most stable and performant APIs using plain futures.

In that regard, prefix keyword await and/or mixed macro await!(...)/.await!() are the most readable, familiar and easy to debug options.

@andreytkachenko

I saw your posting and totally agree, in fact, now that I reflect on "familiarity" ,I think there are two types of familiarity:

  1. Making the await syntax look like similar languages that use alot of await. Javascript is pretty big here, and very relevant to our WASM capabilities as a community.

  2. The second type of familiarity is making code look familiar to devs who only ever work with synchronous code. I think the greatest aspect of await is making async code LOOK synchronous. This is actually one thing javascript really does wrong is that it's normalized long then chains which look utterly foreign to primarily synchronous devs.

One thing i'd offer from my javascript async days, what was far more useful to me as a dev in retrospect was the ability to group promises/future's together. Promise.all(p1(),p2()) so I could easily parallize work. The then() chaining was always just an echo of Javascript's promise past, but pretty much archaic and unecessary now that I think about it.

I'd offer maybe this idea of await. "Try to make the differences between async code and sync code as minimal as possible"

@novacrazy The async function returns an impl Future type, right? What prevents us from adding the await method to Future trait? Like this:

pub fn await(self) -> Self::Output {
    await self
}
...

@XX As I understand it, async functions in Rust are transformed into state machines using generators. This article is a good explanation. So await needs an async function to work in, so the compiler can correctly transform them both. await cannot work without the async part.

Future does have a wait method, which is similar to what you suggest, but blocks the current thread.

@skade,

But how code style could affect type inference? I can't see difference in the same code that's written in chained and imperative style. Types should be exactly the same. If debugger isn't able to figure them out that's definitely problem in debugger, not in code.

@skade, @Matthias247, @XX

With combinators you will have exactly one suspended point marked on beginning of function chain. All other would be implicit inside. That's exactly the same problem as with taking implicit mut, which personally for me was one the biggest point of confusion in Rust at all. Some API would return futures while other would return results of futures - I don't want that. Suspension points should be explicit if possible and properly composable syntax would encourage that

@I60R let bindings are junction points to type inference and help in error reporting.

@novacrazy

So await needs an async function to work in

Can this be expressed in return type? For example, that the return type is impl Future + Async, instead of impl Future.

@skade I've always think that . in method call syntax serves exactly the same purpose

@dpc

I don't think in C# you'd get as much chained/functional code as you would in Rust. Or am I wrong? That makes experiences of C# devs/users interesting, but not necessarily applicable for Rust. I wish we could contrast with Ocaml or Haskell.

you get as much as Rust. Look at any LINQ code and you will see it.

Typical async code looks like this:

async Task<List<IGroping<int, PageMetadata>>> GetPageMetadata(string url, DbSet<Page> pages)
{
    using(var client = new HttpClient())
    using(var r = await client.GetAsync(new Uri(url)))
    {
        var content = await r.Content.ReadAsStringAsync();
                return await pages
                   .Where(x => x.Content == content)
                   .Select(x => x.Metadata)
                   .GroupBy(x => x.Id)
                   .ToListAsync();
    }
}

Or, more general

let a = await!(service_a);
let b = await!(some_method_on(a, some, other, params));
let c = await!(combine(somehow, a, b));

You don't chain calls, you assign it to a variable and then use somehow. It's especially true when you deal with borrowchecker.


I can agree that you can chain futures in one single situation. When you want to handle an error that could result during call. e.g. let a = await!(service_a)?. This is the only situation where postfix alternative is better. I could see it benefit here, but I don't think it outweight all the cons.

Another reason: what's about impl Add for MyFuture { ... } and let a = await a + b; ?

I would like to remind about Generalized Type Ascription RFC in the context of postfix await keyword. It would allow the following code:

let x = (0..10)
    .map(some_computation)
    .collect() : Result<Vec<_>, _>
    .unwrap()
    .map(other_computation) : Vec<usize>
    .into() : Rc<[_]>;

Very much like the postfix await keyword:

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

With the same downsides when badly formatted:

foo.iter().map(|x| x.bar()).collect(): Vec<_>.as_ref()
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

I think if we swallow the pill of Type Ascription RFC, we should swallow fut await for consistency.

BTW, did you know this is a valid syntax:

fn main() {
    println
    !("Hello, World!");
}

Yet I saw zero occurrences of this in real code.

Please allow me to digress a bit. I think we should give postfix macros, as suggested by @BenoitZugmeyer, another thought. Those could be done either as expr!macro or as expr@macro, or maybe even expr.macro!(). I think the first option would be preferable. I'm not sure if they are a good idea, but if we want to extract a general concept and not an ad-hoc solution while still getting a postfix await, we should at least think about postfix macros as a potential solution.

Keep in mind that even if we were to make await a postfix macro, it would still be a magical one (like compile_error!). However, @jplatte and others have already established that that's not an issue.

How I would go about it if we were to go that route, I would first establish exactly how postfix macros would work, then only allow await as a magical postfix macro and then later allow own defined postfix macros.

Lexing

As for lexing/parsing, this might be a problem. If we look at expr!macro, the compiler might think that there's a macro called expr! and then theres some invalid letters macro after that. However, expr!macro should be possible to lex by a lookahead of one, and something becomes a postfix macro when there's an expr followed by a !, directly followed by identifier. I'm not a language developer and I'm not sure if this makes the lexing overly complex. I will just assume that postfix macros can take the form of expr!macro.

Would postfix macros be useful?

On the top of my head, I've come up with these other use cases for postfix macros. I don't think all of them can be implemented by custom macros, though, so I'm not sure if this list is super helpful.

  • stream!await_all: for awaiting streams
  • option!or_continue: when option is None, continue the loop
  • monad!bind: for doing =<< without binding it to a name

stream!await_all

This allows us to not only await futures, but also streams.

event_stream("ws://some.stock.exchange/usd2eur")
    .and_then(|exchange_response| {
        let exchange_rate = exchange_response.json()?;
        stream::once(UpdateTickerAction::new(exchange_rate.value))
    })

would be equivalent to (in a async-stream-esque block):

let exchange_rate = event_stream("ws://some.stock.exchange/usd2eur")
    !await_all
    .json()?;

UpdateTickerAction::new(exchange_rate.value)

option!or_continue

This allows us to unwrap an option, and if it's None, continue the loop.

loop {
    let event = match engine.event() {
        Some(event) => event,
        None => continue,
    }
    let button = match event.button() {
        Some(button) => button,
        None => continue,
    }
    handle_button_pressed(button);
}

would be equivalent to:

loop {
    handle_button_pressed(
        engine.event()!or_continue
            .button()!or_continue
    );
}

monad!bind

This one would allow us to get monads in a fairly rustic (i.e. expression centered) way. Rust doesn't have anything like monads yet and I'm not sold on them being added to Rust. Nevertheless, if they were to be added, this syntax might be useful.

I took the following from here.

nameDo :: IO ()
nameDo = do putStr "What is your first name? "
            first <- getLine
            putStr "And your last name? "
            last <- getLine
            let full = first ++ " " ++ last
            putStrLn ("Pleased to meet you, " ++ full ++ "!")

would be eqivalent to:

do {
    putStr("What is your first name? ")!bind;
    let first = getLine()!bind;
    putStr("And your last name? ")!bind;
    let last = getLine()!bind;
    let full = first + " " + &last
    putStrLn("Pleased to meet you, " + &full + "!")!bind;
}

Or, more inlined, with less lets:

do {
    putStr("What is your first name? ")!bind;
    let first = getLine()!bind;
    putStr("And your last name? ")!bind;
    putStrLn(
        "Pleased to meet you, " + &first + " " + &getLine()!bind + "!"
    )!bind;
}

Evaluation

I'm very split on this. The mathematician part of my brain wants to find a generalized solution for await, and postfix macros might be a way to achieve them. The pragmatic part of my brain thinks: Why bother making a language whose macro system already nobody understands even more complicated.

However, from ? and await, we have two examples of super useful postfix operators. What if we find other ones we want to add in the future, maybe akin to those I mentioned? If we've generalized them, we can add those to Rust naturally. If we haven't, we'd have to come up with yet another syntax each time, which would likely bloat the language more so than the postfix macros would have.

What do you guys think?

@EyeOfPython

I have came up to very similar idea, and sure that postfix macros in Rust is matter of time.
I am thinking about it every time when dealing with ndarray slicing:

let view = array.slice(s![.., ..]);

but much better would be

let view = array.slice![.., ..];
// or like you suggested
let view = array!slice[.., ..];
// or like in PHP
let view = array->slice![.., ..];

and a lot of combinators may gone, like ones with _with or _else postfixes:

opt!unwrap_or(Error::new("Error!"))?; //equal to .unwrap_or_else(||Error::new("Error!"));

@EyeOfPython @andreytkachenko postfix macros are currently not a feature in Rust and IMHO would need a complete RFC+FCP+Implementation phase.

This is not an RFC discussion, but a discussion of an accepted RFC which needs to be implemented.

For that reason, I don't think its practical to discuss them here or propose them for async syntax. It would further delay the feature massively.

To curb this already massive discussion, I think it's _not_ useful to discuss them here, they can be seen as outside of the discussed issue.

Just some thought: While this thread is specifically for await, I think we'll have the very same discussion for yield expressions down the road, which likewise could be chained. Inasmuch I'd prefer to see a syntax that can be generalized, however it looks.

My reasons to not use macros here:

  1. Macros are provided for domain-specific stuff and using them in any ways that changes programs control flow or emulates core language features is an overkill
  2. Postfix macros immediately would be abused to implement custom operators or other esotheric language features that leads to bad code
  3. They would discourage developing proper language features:

    • stream.await_all is perfect use case for combinator

    • option.or_continue and replacing _else combinators is perfect use case for null coalescing operator

    • monad.bind is perfect use case for if-let chains

    • ndarray slicing is perfect use case for const generics

  4. They would put under question already implemented ? operator

@collinanderson

which likewise could be chained

Why on the earth you think they could be chained? zero occurances in real code, just like println example above.

@Pzixel It is likely that eventually Generator::resume will take a value, and thus yield expr will have non-() type.

@vlaff I don't see clearly see an argument for await having to be consistent with Type Ascription. They are very different things.

Also, Type Ascription is another of those features that are repeatedly tried, there is no guarantee that _this_ one will make it. While I don't want to oppose this, TA is a future RFC and this is a discussion about an accepted feature with an already proposed syntax.

Reading through the many comments, to add further summary on why await!(...) appears a very ideal path:

  1. it looks the most familiar to existing code because macros are familiar
  2. there's existing work that uses await!(...) https://github.com/alexcrichton/futures-await and could help reduce code rewrite
  3. since postfix macros are not on the table, may never be a part of the language, and "await!" in a non standard context does not seem like it's even a possibility as per RFCs
  4. it gives the community more time to consider the long term direction after wide spread stable usage while providing something that is not totally out of the norm (eg. try!() )
  5. could be use as a similar pattern for upcoming yield!() until we figure out an official path that could satisfy await and yield
  6. chaining may not be as valuable as it's hoped for and clarity probably be even enhanced by having multiple awaits on multiple lines
  7. no changes would have to happen for IDE syntax highlighters
  8. people from other languages probably will not be thrown off by an await!(...) macro after seeing other macro usages ( less cognitive overload )
  9. it's probably the most minimal effort path of all paths to move forward to stabilization

await! macro is alredy here, the question is about chaining.

The more I think about it, the more postfix look good to me. For example:

let a = foo await;
let b = bar await?;
let c = baz? await;
let d = booz? await?;
let e = kik? + kek? await? + kuk? await?;
// a + b is `impl Add for MyFuture {}` which alises to `a.select(b)`

Allows nesting of any level, and quire readable. Works seamless with ? and possible future operators. It look a bit alien for newcomers, but grammar profit may outweight it.

The main reason is await should be a separate keyword, not a function call. It's too important so it should have its own highlight and place in the text.

@Pzixel , @HeroicKatora , @skade

See for example Python yield and yield from expressions: there the outer function can provide a value that will become the result of the yield when the generator resumes. This is what @valff meant, and has nothing to do with type ascriptions. Hence, yield will also have a type other than ! or ().

From the point of the coroutine, both yield and await suspend it and can (eventually) return a value. Inasmuch they are merely 2 sides of the same coin.

And to throw out another syntax possibility, _square-brackets-with-keyword_, partly to highlight this (using yield for the syntax highlighting):

let body: MyResponse = client.get("http://api").send()[yield]?.into_json()[yield]?

"postfix keyword" makes the most sense to me. postfix with a character other than whitespace separating the future-expression and await also makes sense to me but not '.' as that's for methods and await is not a method. However, if you want await as a prefix keyword I like the @XX suggestion of a Trait or method that just calls await self for those that want to chain a bunch of things together (although we probably couldn't name the method await; just wait would do though I think). Personally, I'd probably end up making an await-per-line and not chain so much because I find long chains less readable so prefix or postfix would work for me.

[edit] I forgot that wait already exists and blocks the future, so scratch that thought.

@roland I'm specifically referring to this, which discusses type ascriptions: https://github.com/rust-lang/rust/issues/57640#issuecomment-456023146

@rolandsteiner so you write

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

When I'd write it like:

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

@skade Oh, you meant a different comment than I thought, sorry. :stuck_out_tongue:

@Pzixel It is likely that eventually Generator::resume will take a value, and thus yield expr will have non-() type.

Hence, yield will also have a type other than ! or ().

@valff, @rolandsteiner I find it unlikely that yield will return the resuming value, that is difficult to fit into a statically typed language without making the generator syntax and/or trait annoying to work with. The original prototype with resume arguments used the keyword gen arg to refer to this argument, something like that is much more likely to work well IMO. From that point of view yield will still return () so shouldn't affect the discussion of await much.

I think await should either be a prefix operator, because async is (and I expect yield to be prefix as well). Or have it as a regular method, used in postfix position, then.

However, I don’t get all the bikeshedding running on here: why even considering a postfix keyword where no other of Rust has it? It would make the language so weird.

Also, chaining futures, I get why it could be interesting. But chaining awaits? What does it even mean? A future that returns a new future? Is it really a so common idiom we want Rust to have that idiom as first-class citizen? If we really care about that, I think we should:

  1. Go for the method (i.e. foo.await()). It doesn’t introduce a weird postfix keyword and we all know what it means. We can also chain with that.
  2. If we really want a keyword / loloperator, we can settle up on that question later.

Also, for the sake of giving my personal opinion, I hate the await in postfix keyword position.

Prefix await is used in other languages for a reason - it matches the natural language. You don't say "I will your arrival await". Though having written JavaScript I may be a little biased.

I will also say that I consider await-chaining overrated. The value of async/await over future combinators is that it allows one to write asynchronous code sequentially, utilizing the standard language constructs such as if, match, et cetera. You are going to want to break up your awaits anyway.

Don't let's make up too much magic syntax for async/await, it's only a small part of the language. The proposed method syntax (i.e. foo.await()) is IMO too orthogonal to normal method calls and comes off as too magical. The proc-macro machinery is in place, why not disguise it behind that?

How about use await keyword as opposed to async in function defenition? For example:

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future
}

This await fn can only be called in async context:

async {
    let n = foo(bar());
}

And desugaring to let n = (await foo(bar()));.
Then in addition to the await keyword, trait Future could implement the await-method for using await logic in postfix position, for example:

async {
    let n = bar().awaited();
}

Also, can someone explain me the relationship with generators? I’m surprised async / await are implemented before generators are (even stabilized).

@phaazon generators have been added to Rust as an internal implementation detail for async/await. There are no current plans to stabilize them, and they still require a non-experimental RFC before they can be. (Personally I would love to have them stabilized, but it seems likely they'll be at least a year or two away, probably not being RFCed until after async/await is stable and there's been some experience with it).

@XX I think you’re breaking the semantics of async if you use await in the function definition. Let’s stick to standards, don’t we?

@phaazon Can you specify in more detail how this breaks semantics of async?

Most languages use async to introduce something that is going to be asynchronous and await awaits for it. So having await instead of async is kinda weird, to me.

@phaazon No, not instead, but in addition. Full example:

async fn bar() -> i32 {
    5 // will be "converted" to impl Future<Output = i32>
}

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future // will be "converted" to i32 in async context
}

async {
    let a = await bar(); // correct, a == 5
    let b = foo(bar()); // correct, b == 5
}

let c = foo(bar()); // error, can't call desugaring statement `await foo(bar())`

if it is valid, then it will be possible to implement await-method for use in a chain:

async {
    let n = first().awaited()?.second().awaited()?;
    // let n = (await (await first())?.second())?;
}

In my opinion, it doesn't matter if postfix await "feels" too magical and not familiar to the language. If we adopt the method syntax .await() or .await!() then it's just a matter of explaining that Futures have an .await() or .await!() method that you can also use to await them in the cases that make sense. It's not that hard to understand if you spend 5 minutes thinking about it, even if you've never seen it before.

Besides, if we go with the prefix keyword, what is stopping us from writing a crate with the following (incomplete pseudocode because I don't have the infrastructure to deal with the details right now):

trait AwaitChainable {
    fn await(self) -> impl Future;
}

impl<T: Future> AwaitChainable for T {
    fn await(self) -> impl Future {
        await self
    }
}

That way we can get postfix await if we want. I mean, even if that is somehow not possible and we have to implement postfix await magically by the compiler, we can still use something like the example above to explain how it mostly works. It wouldn't be hard to learn.

@ivandardi I thought about the same. But this definition

fn await(self) -> impl Future {
    await self
}

does not say that the function can be called in async-context only.

@XX Yeah, as I said, ignore the broken pseudocode :P I currently don't have the infrastructure to look up how the correct syntax and traits should be written there :( Just imagine that I wrote the thing that makes it only callable in async contexts.

@XX, @ivandardi Per definition, await only works in an async context. Therefore this is illegal:

fn await(self) -> impl Future {
    await self
}

It must be

async fn await(self) -> impl Future {
    await self
}

Which can only be called in an async context like this:

await future.await()

Which sort of defeats the whole purpose.

The only way to make this work is to change async completely, which is off the table.

@ivandardi This evolution of my version:

fn await(self) -> T { // How to indicate using in async-context only?
    await self
}

```rust
await fn await(self) -> T { // Because async already taken
await self
}

```rust
await fn await(self) -> T {
    self // remove excess await
}

@CryZe @XX Why are you booing me? I'm right.

@XX What you're trying to do is to alter the meaning of async and await completely (e.g. create an await context that's somehow different from an async context). I don't think it would get much support from the language developers

@EyeOfPython This definition doesn't work as expected:

async fn await(self) -> impl Future {
    await self
}

More likely:

#[call_only_in_async_context_with_derived_await_prefix]
fn await(self) -> impl Future {
    self
}

I'm downvoting because you ignored that this is hypothetical pseudo syntax. Essentially their point is that Rust could add a special trait like Drop and Copy that the language understands, which adds the ability to call .await() on the type which then interacts with the type just like an await keyword would. Also in addition voting is considered irrelevant for RFCs anyway, as the point is to figure out an objective solution, not one based on subjective feelings such as visualized by upvotes / downvotes.

I don’t understand what this code should do. But is has nothing to do with how async await currently works. If you care about it, please keep it out of this thread and write a dedicated RFC for it.

@CryZe That type is available. It’s called Future. And it was agreed upon a long time ago that async/await is a mechanism which is ONLY targeted at transforming async code. It is not meant as a general bind notation.

@ivandari your postfix await doesn’t await but creates a new future. It’s essentially an identity function. You can’t await implicitly, because it’s not a regular function.

@Matthias247 What they were trying to suggest was a way to have postfix await have the syntax of a method call. That way Rust does not need to introduce arbitrary new symbols such as #, @ or ... like we did with the ? operator, and still look fairly natural:

let result = some_operation().await()?.some_method().await()?;

So the idea would be to somehow have await be a special kind of method on the Future trait that the compiler sees the same way as a normal await keyword (which you don't need to have at all in this case) and transform the async code into a generator from there. Then you have the proper logical control flow from left to right instead of left right left with await some_future()? and don't need to introduce weird new symbols.

(So tl;dr: it looks like a method call but is actually just postfix await)

Something I haven't seen mentioned explicitly in consideration of a postfix syntax is the prevalence of the error and option combinators. These are the place where rust is the most different from all the languages that have await -- instead of automatic tracebacks, we have .map_err()?.

In particular things like:

let result = await reqwest::get(..).send();
let response = result.map_err(|e| add_context(e))?;
let parser = await response.json();
let parsed = parser.map_err(|e| add_context(e))?;

is less readable than (I don't care about which postfix syntax is considered):

let parsed = reqwest::get(..)
    .send() await
    .map_err(add_context)?
    .json() await
    .map_err(add_context)?;

_even though_ this default formatting has more lines than the multi-variable approach. Expressions that don't have multiple awaits seem like they should take up less vertical space with postfix-await.

I don't write async code right now, so I'm speaking from ignorance, sorry to pile on -- if this isn't a problem in practice, then that's excellent. I just don't want proper error-handling to be made less-ergonomic just to be more similar to other languages that do error handling differently.

Yes, that's what I mean. It would be the best of both worlds, allowing people to choose whether to use prefix or postfix await depending on the situation.

@XX If that can't be expressed in user code, then we'll have to resort to the compiler to implement that functionality. But in terms of understanding how the postfix works, the explanation I posted previously still works, even if just a superficial understanding.

@CryZe, @XX,

On first sight it loosk smooth, but absolutely not matches Rust philosophy. Even sort method on iterators don't returns self and breaks method call chain to make allocation explicit. But you expects something non less impactful to be implemented in completely implicit fashion indistinguishable from regular function calls. There's no chances IMO.

Yes, that's what I mean. It would be the best of both worlds, allowing people to choose whether to use prefix or postfix await depending on the situation.

Why should that be a goal? Rust doesn't support this for all other control flow (e.g. break, continue, if, etc either. There isn't a particular reason, apart from "this might look nicer in ones particular opinion.

In general I would like to remind everyone that await is quite special and not a normal method invocation:

  • Debuggers may behave weird when stepping over awaits, because the stack might unwind and get reestablished. As far as I remember it took a lot of time to get async debugging working in C# and Javascript. And those have paid teams who work on debuggers!
  • Local objects that don't make it across await points can be stored on the real OS stack. Ones that aren't must be moved into the generated future, which will in the end make a difference in required heap memory (which is where the Future will live).
  • Borrows across await points are the reason why generated futures are required to be !Unpin, and cause a lot of inconvenience with some of the existing combinators and mechanisms. It might be potentially possible to generate Unpin futures from async methods that don't borrow across awaits in the future. However if awaits are invisible, that will never happen.
  • There might be other unexpected borrow checker issues, that are not covered by the current state of async/await.

@Pzixel @lnicola

The examples you give for C# are totally imperative, and nothing like functional style long-chains we often see in Rust. That LINQ syntax is just a DSL, and is nothing like foo().bar().x().wih_boo(x).camboom().space_flight(); I've Googled around a bit, and C# code examples look 100% imperative, just like most of popular programming languages. That's why we can't just take what langue X did, because it's just not the same.

IMO prefix notation fits perfectly in the imperative style of coding. But Rust supports both styles.

@skade

I disagree with some of yours (and others) comments about problems with functional style. To keep it brief - let's just agree that there are people with strong preference towards one or the other.

@Matthias247 No, there is a particular reason other than how nice it looks. It's because Rust encourages chaining. And you might say "oh, other control flow mechanisms don't have a postfix syntax, so why should await be special?". Well, that is a wrong assessment. We DO have postfix control flow. They're just internal to the methods we call. Option::unwrap_or is like doing a postfix match statement. Iterator::filter is like doing a postfix if statement. Just because the control flow isn't part of the chaining syntax itself, it doesn't mean we don't already have postfix control flow. In that point of view, adding a postfix await would actually be consistent to what we have. In this case, we could even have something more similar to how Iterator works and instead of just having a raw postfix await, we could have some await combinators, like Future::await_or. Either way, having postfix await isn't just a matter of looks - it's a matter of functionality and quality of life. Otherwise we'd still be using the try!() macro, right?

@dpc

The examples you give for C# are totally imperative, and nothing like functional style long-chains we often see in Rust. That LINQ syntax is just a DSL, and is nothing like foo().bar().x().wih_boo(x).camboom().space_flight(); I've Googled around a bit, and C# code examples look 100% imperative, just like most of popular programming languages. That's why we can't just take what langue X did, because it's just not the same.

It's not true (you could check any framework, e.g. polly), but I won't argue about it. I see as many chained code in both languages. However, there actually is a thing that makes everything different. And it's called Try trait. C# has nothing similar to it so it doesn't suppose to do anything past await point. If you get an exception, it will be automatically wrapped and raised for caller.

It's not the case for Rust, where you have to manually use ? operator.

If you have to have anything past await point you should either create "combined" operator await?, extend language rules etc etc OR you can just make await postfix and things become more natural (except for a little bit alien syntax, but who cares?).

So I currently see postfix await as more viable solution. The only suggestion I have there should to be a dedicated space-separated keyword, not await() or await!().

I think I came up with a good reason justifying a normal .await() method. If you think about it, it's not any different than any other way of blocking. You call the .await() method, the (possibly green) thread's execution gets parked and at some point the executing runtime then resumes execution of the thread at some point. So for all intents and purposes, whether you have a Mutex or std channel like so:

let result = my_channel().recv()?.iter().map(...).collect();

or a future like so:

let result = my_future().await()?.iter().map(...).collect();

is not any different. They both block execution at their respective recv() / await() and they both chain the same way. The only difference is that await() is possibly running on a different executor that is not the OS's heavy weight threading (but it may very well be in case of a single threaded executor). So discouraging heavy amounts of chaining affects both the exact same way and should probably lead to an actual clippy lint.

However my point here is that from the person that is writing this code's point of view, there isn't much of a difference in .recv() or .await(), both are methods that block execution and return once the result is available. So for all intents and purposes it can pretty much be a normal method and doesn't require a full blown keyword. We don't have a recv or lock keyword for Mutex and std's channel either.

However rustc obviously wants to turn the whole code into a generator. But is that actually semantically necessary from a language point of view? I'm pretty sure one could write an alternative compiler to rustc that implements await via blocking an actual OS thread (entering an async fn would need to spawn a thread and then awaiting would block that thread). While that would be an extremely naive and slow implementation, it would semantically behave the exact same way. So the fact that actual rustc turns await into a generator is not necessary semantically. So I would actually argue that rustc turning the call to the .await() method into a generator can be seen as an optimizing implementation detail of rustc. That way you can justify .await() being kind of a full method and not be a full keyword, but also still have rustc turn the whole thing into a generator.

@CryZe unlike .recv() we can safely interrupt await from any other place in program and then code next to await wouldn't be executed. That's a big difference and that's the most valuable reason why we shouldn't do await implicitly.

@I60R It's not any less explicit than await as a keyword. You can still highlight it the same way in the IDE if you wanted to. It just moves the keyword to postfix position, where I would argue it's actually less easy to miss (as it is exactly in the position where the execution halts).

Crazy idea: implicit await. I've moved to a separate thread, because this one is already too busy with postfix vs prefix discussion.

@CryZe

I think I came up with a good reason justifying a normal .await() method. If you think about it, it's not any different than any other way of blocking.

Please read https://github.com/rust-lang/rust/issues/57640#issuecomment-456147515 again

@Matthias247 These are very good points, seems like I missed those.

All this talk about making await into a function, member or otherwise implicit is fundamentally invalid. It isn’t an action, it’s a transformation.

The compiler, at a high level, be it through a keyword or macro, transforms await expressions into special generator yield expressions, in-place, accounting for borrows and other stuff along the way.

This should be explicit.

It should either be as apparant as possible as a keyword, or obviously generating code with a macro.

Magic methods, traits, members, etc., are all too easy to misread and misunderstand, especially to new users.

Prefix keyword await or prefix macro await seem like the most acceptable way of doing this, which makes sense as many other languages do it that way. We don’t have to be special to make it good.

@novacrazy

Magic methods, traits, members, etc., are all too easy to misread and misunderstand, especially to new users.

Can you expand on that? More specifically on the .await!() variant, if possible.

Having a foo.await!() isn't hard to read, in my opinion, ESPECIALLY with proper syntax highlighting, which shouldn't be ignored.

Misunderstand the meaning of it? What is does is essentially the following (ignore the type mystakes):

trait Future {
    fn await!(self) -> Self::Item {
        await self
    }
}

AKA await this_foo and this_foo.await!() are exactly equal. What is easy to misunderstand about that?

And on the topic of new users: new users to what? Programming in general, or new users to Rust as a language but with a programming language background? Because if the former, then I doubt they'll dabble on async programming right on. And if the latter, then it's easier to explain the semantics of postfix await (as explained above) without confusion.

Alternatively, if only prefix await gets added, nothing that I know of stops the creation of a program that takes as input Rust code of form

foo.bar().baz().quux().await!().melo().await!()

and transforms it into

await (await foo.bar().baz().quux()).melo()

@ivandardi

.await!() is nice for some cases, and would probably work alongside await!(...) like:

macro_rules! await {
    // prefix
    ($fut:expr) => {...}

    // postfix
    ($self:Self) => { await!($self) }
}

However, postfix method macros don't exist right now, and may never exist.

If we think that is a possibility in the future, we should go with the macro await!(...) for now and simply add the postfix in the future when that is implemented.

Having both macros would be ideal, but if there is no intention of implementing postfix macros in the future, prefix keyword await is probably the best bet.

@novacrazy I can agree with that and it is my original proposal. We should add await!() for now and figure out a postfix form as we go. Potentially also discuss the possibility of postfix macros too, and if we could ad-hoc a postfix .await!() into the language before having full postfix macro support in the language. Kinda like what happened with ? and the Try trait: it got added first as a special case and afterwards it's being expanded into a more general case. The only thing we'd need to be careful when deciding is how the general postfix macro syntax would look like, which might deserve a separate discussion.

Prefix keyword await or prefix macro await seem like the most acceptable way of doing this, which makes sense as many other languages do it that way.

It's obviously _workable_, but I only remember two arguments for it, and I don't find them persuasive:

  • That's how other languages do it

    • That's nice, but we're already doing things differently like not-running-unless-polled instead of running-up-to-the-first-await, because rust is a fundamentally different language

  • People like seeing await at the beginning of the line

    • But it's not always there, so if that's the only place one looks one will have problems

    • It's just as easy to see the awaits in foo(aFuture.await, bFuture.await) as if they were prefix

    • In rust one is already scanning the _end_ of the line for ? if you'll looking at control flow

Did I miss something?

If the debate was "meh, they're all about the same", I'd absolutely agree with "well, if we don't really care we might as well do what everyone else does". But I don't think that's where we are.

@scottmcm Yes to, "meh, they're all about the same." At least from the user's perspective.

So we should find a decent balance of readability, familiarity, maintainability and performance. Therefore, my statement in my last comment holds true, with macro await!() if we intend to add postfix method macros (.await!()) in the future, or just a boring prefix keyword await otherwise.

I say boring, because boring is good. We want to keep our mind away from the syntax itself when writing code with these.

If f.await() would not be a good idea, then I prefer the prefix syntax.

  1. As a user, I hope the language I use have only a few syntax rules, and using these rules, I can reliably infer what it might be doing. NOT exceptions here and there. In Rust, async is at beginning, await at beginning won't be an exception. However, f await, post keyword form would be. f.await looks like field access, an exception. f.await!() has postfix macro never appeared in the language, and don't know what other cases it would be good for, an exception. We don't have answer how these syntaxes would become rules not one-time exceptions.

  2. When exception is made, I hope it make intuitive sense. Take ? as an example, which can be seen as exception because not frequent in other languages. f()?.map() reads almost like compute f() and is this result a good one? The ? here explains itself. But for f await I ask why it's postfix?, for f.await I ask is await a field, f.await!() I ask why macro appear in that position? They don't provide a convincing/intuitive sense for me, at least at first glance.

  3. Extending the first point, Rust would most like to be a systems language. The major players here, C/C++/Go/Java are all somewhat imperative. I also believe most guys starts their career with an imperative language, C/Python/Java not Haskell, etc. I guess to persuade systems developers and next generation developers to adopt Rust, Rust must first do well in imperative style and then functional, not being highly functional but don't have a familiar imperative feeling.

  4. I think there is nothing wrong to split a chain and write it into several lines. It won't be verbose. It's just explicit.

When seeing how discussion continuously moves from one direction into another (prefix vs postfix) I developed a strong opinion that there's something wrong with await keyword and we must retreat from it completely. Instead I propose the following syntax that I think will be a very good compromise among all of opinions that were published in current thread:

// syntax below is exactly the same as with prefix `await`
let response = go client.get("https://my_api").send();
let body: MyResponse = go response.into_json();

On first step we would implement it just as regular prefix operator without any error handling specific superstructures:

// code below don't compiles because `?` takes precedence over `go`
let response = go client.get("https://my_api").send()?;
let body: MyResponse = go response.into_json()?;

On second step we would implement deferred prefix operator syntax that would allow for proper error handling as well.

// now `go` takes precedence over `?` if present
let response = client.get("https://my_api").go send()?;
let body: MyResponse = response.go into_json()?;

That's all.


Now let's see some extra examples that provides more context:


// A
if db.go is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.go load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .go send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .go send()?
    .error_for_status()?
    .go json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .go send()?
    .error_for_status()?
    .go json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.go request(url, Method::GET, None, true)?
        .res.go json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   go self.logger.log("beginning service call");
   let output = go service.exec();
   go self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = go acquire_lock();
    let length = logger.go log_into(message)?;
    go logger.timestamp();
    Ok(length)
}

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

/// J
let res = client.get("https://my_api").go send()?.go json()?;

There's hidden under spoiler version with syntax highlighting enabled


// A
if db.as is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.as load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .as send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .as send()?
    .error_for_status()?
    .as json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .as send()?
    .error_for_status()?
    .as json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.as request(url, Method::GET, None, true)?
        .res.as json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   as self.logger.log("beginning service call");
   let output = as service.exec();
   as self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = as acquire_lock();
    let length = logger.as log_into(message)?;
    as logger.timestamp();
    Ok(length)
}

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

/// J
let res = client.get("https://my_api").as send()?.as json()?;


IMO this syntax is better than alternatives in every aspect:

✓ Consistency: looks very organic inside of Rust code and don't requires breaking code style
✓ Composability: integrates well with other Rust features like chaining and error handling
✓ Simplicity: is either short, descriptive, easy to understand, and easy to work with
✓ Reusability: deferred prefix operator syntax would be useful in other contexts as well
✓ Documentation: implies that call side dispatches flow somewhere and waits until it returns
✓ Familiarity: provides already familiar pattern but does that with less tradeoffs
✓ Readability: reads as plain English and don't distorts meaning of words
✓ Visibility: its position makes it very hard to be disguised somewhere in code
✓ Reachability: could be very easily googled
✓ Well-tested: golang is popular nowadays with somehow similar looking syntax
✓ Surprises: it's hard to misunderstood as well as abuse that syntax
✓ Gratification: after small amount of learning every user will be satisfied with result


Edit: as @ivandardi pointed in comment below, some things should be clarified:

1. Yes, this syntax is a bit hard to eye-parse, but at the same time it's impossible to invent syntax here that wouldn't have any readability problems. go syntax for me seems to be less evil here, since In prefix position it's exactly the same as with prefix await, and in deferred position it's IMO more readable than postfix await e.g:

match db.go load(message.key) await {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

where not await looks ambiguous and has different associativity than all of existed keywords, but also could become blocker if we decide to implement await blocks sometime in future.

2. It will require creating RFC, implementing and stabilizing of "deferred prefix operator" syntax. This syntax would be not specific to async code, however using it in async would be primary motivation for it.

3. In "deferred prefix operator" syntax keyword matter because:

  • long keyword would increase space between variable and method which is bad for readability
  • long keyword is strange choice for prefix operator
  • long keyword is unnecessarily verbose when we want async code to look as sync

Anyway, it's quite possible to start use await instead of go and then rename it in 2022 edition. To that time it's very likely that different keyword or operator will be invented. And we can even decide that no rename is required at all. I've found await to be readable too.

There's hidden under spoiler version with await used instead of go


// A
if db.await is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.await load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .await send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .await send()?
    .error_for_status()?
    .await json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .await send()?
    .error_for_status()?
    .await json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.await request(url, Method::GET, None, true)?
        .res.await json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   await self.logger.log("beginning service call");
   let output = await service.exec();
   await self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = await acquire_lock();
    let length = logger.await log_into(message)?;
    await logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    await (as partial_computation()).unwrap_or_else(or_recover);
}

/// J
let res = client.get("https://my_api").await send()?.await json()?;


If you are here to downvote, ensure:

  • that you understood it properly, since I might be not a best speaker for explaining new things
  • that your opinion isn't biased, sine there's a lot of prejudices from where that syntax originates from
  • that you will put a valid reason below, since silent downvotes are extremely toxic
  • that your reason isn't about immediate feelings, since I've tried to design long-term feature here

@I60R

I do have a few gripes with that.

match db.go load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

That is slightly hard to eye-parse. At first glance you'd think we'd be matching on db.go, but then there's something more to that and you go "oooh, ok, load is a method of db". IMO that syntax primarily won't work because methods should always be close to the object they belong to, with no space and keyword interruption between them.

Second of all, your proposal requires defining what "deferred prefix operator syntax" is and possibly generalizing it in the language first. Otherwise it will be something only used for async code, and we're back to the discussion of "why not postfix macros?" too.

Thirdly, I don't think the keyword matters at all. However, we should favor await as the keyword has been reserved for the 2018 edition, whereas the go keyword has not and should undergo a deeper crates.io search before being proposed.

When seeing how discussion continuously moves from one direction into another (prefix vs postfix) I developed a strong opinion that there's something wrong with await keyword and we must retreat from it completely.

We could have (semi-)implicit await and everything would be just fine, but it's going to be a hard sell in Rust community, even despite the fact that the whole point of async functions is to hide the implementation details and make them look just like blocking io one.

@dpc I mean, the best way I can think of that works as a compromise between semi-implicit await and the desire to have code sections being explicitly awaited is something similar to unsafe.

let mut res: Response = await { client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()?
    .error_for_status()?
    .json()?
};

Where everything inside the await block would automatically be awaited. That solves the need for postfix await for chaining, since you can just await the whole chain instead. But at the same time, I'm not sure if that form would be desirable. Maybe someone else can find a really good reason against this.

@dpc The point of async functions is not to hide the details, it's to make them more approachable and easier to use. We should never hide potentially costly operations, and should take advantage of explicit futures where possible.

No one confuses async functions with synchronous ones, in any language. It's purely to organize sequences of operations in a more readable and maintainable form, rather than hand-writing a state machine.

Also, implicit awaits as shown in @ivandardi 's example would make it impossible to use Future combinators, which as I have already shown are still incredibly powerful. We can't just grab any Future and wait on it, that would be highly inefficient.

Explicit, familiar, readable, maintainable, and performant: prefix keyword await or macro await!(), with potentially postfix macro .await!() in the future.

@novacrazy Even if the ideal syntax was chosen, I’d still want to use custom futures and combinators in some situations. The idea that those could be soft deprecated makes me question the entire direction of Rust.

Of course you can continue to use combinators, nothing is being deprecated.

It's the same situation as ?: new code will usually use ? (because it's better than the combinators), but the Option/Result combinators can still be used (and the funkier ones like or_else are still regularly used).

In particular, async/await can completely replace map and and_then, but the funkier Future combinators can (and will) still be used.

The examples you give still look terrible compared to the combinators,

I don't think so, I think they are much clearer, because they use standard Rust features.

Clarity is rarely about number of characters, but it has everything to do with mental concepts.

With postfix await it looks even better:

some_op().await?
    .another_op().await?
    .final_op().await

You can compare that to your original:

some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())

and with the generator overhead will probably be a bit slower and produce more machine code.

Why do you claim that there will be extra overhead from async/await? Both generators and async/await are specifically designed to be zero-cost and heavily optimized.

In particular, async/await compiles into a heavily optimized state machine, just the same as Future combinators.

rather than creating these generator spaghetti state machines under the hood.

The Future combinators also create a "spaghetti state machine" under the hood, which is exactly what they were designed to do.

They are not fundamentally any different from async/await.

[Future combinators] have the potential of compiling down to more efficient code.

Please stop spreading misinformation. In particular, async/await has the possibility to be faster than Future combinators.

If it turns out that users aren’t happy with a prefix keyword, it can be changed in a 2019/2020 edition.

As others have said, editions aren't an arbitrary thing we just make whenever we feel like it, they're specifically designed to only happen every few years (at the earliest).

And as @Centril pointed out, coming up with a bad syntax just to replace it later is a very inefficient way of doing things.

The pace of discussion is quite high right now; so in an effort to slow things down and allow folks to catch up, I've locked the issue temporarily. It will be unlocked within a day. At that time, please try to be as constructive as possible moving forward.

Unlocking the issue a day later... Please consider not making points already made and keep comments on topic (e.g. this is not the place to consider implicit await or other things out of scope..).

As this has been a long thread, let's highlight some of the most interesting comments buried in it.

@mehcode provided us extensive real-world code with await in both prefix and postfix positions: https://github.com/rust-lang/rust/issues/57640#issuecomment-455846086

@Centril argued persuasively that stabilizing await!(expr) amounts to knowingly adding technical debt: https://github.com/rust-lang/rust/issues/57640#issuecomment-455806584

@valff reminded us that the expr await postfix keyword syntax would fit most cleanly with Generalized Type Ascription: https://github.com/rust-lang/rust/issues/57640#issuecomment-456023146

@quodlibetor highlighted that the syntactic effect of Error and Option combinators is unique to Rust and argues in favor of postfix syntax: https://github.com/rust-lang/rust/issues/57640#issuecomment-456143523

At the end of the day, the lang team is going to need to make a call here. In the interest of identifying common ground that might lead to a consensus, perhaps it would be helpful to summarize the stated positions of lang team members on the key issue of postfix syntax:

  • @Centril expressed support for postfix syntax and explored a number of variations in this ticket. Centril particularly does not want to stabilize await!(expr).

  • @cramertj expressed support for postfix syntax and specifically for the expr await postfix keyword syntax.

  • @joshtriplett expressed support for postfix syntax and suggested we should also provide a prefix version.

  • @scottmcm expressed support for postfix syntax.

  • @withoutboats does not want to stabilize a macro syntax. While concerned about exhausting our "unfamiliarity budget," withoutboats considers expr await postfix keyword syntax to have "a real shot".

  • @aturon, @eddyb, @nikomatsakis, and @pnkfelix have not yet expressed any position regarding syntax in issues #57640 or #50547.

So the things we need to figure out a yes/no answer to:

  • Should we have prefix syntax?
  • Should we have postfix syntax?
  • Should we have prefix syntax now and figure out postfix syntax later?

If we go with prefix syntax, there's two main contenders that I feel people are most accepting: Useful and Obvious, as seen in this comment. The arguments against await!() are pretty strong, so I'm considering it ruled out. So we need to figure out if we want Useful or Obvious syntax. In my opinion, we should go for whichever syntax uses the less parenthesis in general.

And as for the postfix syntax, that's trickier. There are also strong arguments for postfix await keyword with space. But that is very dependent on how code is formatted. If the code is poorly formatted, the postfix await keyword with space can look real bad. So first, we'd need to assume that all Rust code will be formatted correctly with rustfmt, and have rustfmt always split chained awaits into different lines, even if they'd fit in a single line. If we can do that, then that syntax is very ok, since it solves the problem of readability and confusion with spaces in the middle of a single-line chain.

And finally, if we do go with both prefix and postfix, we need to figure out and write down the semantics of both syntaxes to see how they interact with each other and how one would be converted to the other. It should be possible to do it, since choosing between postfix or prefix await shouldn't change how the code is run.

To clarify my thought process, and how I approach and evaluate surface syntax proposals wrt. await,
here are the goals I have (without any order of importance):

  1. await should remain a keyword to enable future language design.
  2. The syntax should feel first class.
  3. Awaiting should be chainable to compose well with ? and methods in general since they are prevalent in Rust. One should not be forced into making temporary let bindings which may not be meaningful subdivisions.
  4. It should be easy to grep for await points.
  5. It should should be possible to see await points at a glance.
  6. The precedence of the syntax should be intuitive.
  7. The syntax should compose well with IDEs and muscle memory.
  8. The syntax should be easy to learn.
  9. The syntax should be ergonomic to write.

Having defined (and probably forgotten...) some of my goals, here's a summary and my evaluation of some proposals with respect to those:

  1. Retaining await as a keyword makes it difficult to use a macro based syntax whether it is await!(expr) or expr.await!(). To use a macro syntax, await as a macro either becomes hardcoded and not integrated with name resolution (i.e. use core::await as foo; becomes impossible), or await is relinquished as a keyword entirely.

  2. I believe that macros have a distinctly non-first-class feeling about them as they are meant for inventing syntax in user space. Although it is not a technical point, using macro syntax in such a central construct of the language gives an unpolished impression. Arguably, the .await and .await() syntaxes are not the most first-class syntaxes either; but not as second-class as macros would feel.

  3. The desire to facilitate chaining makes any prefix syntax work poorly whereas postfix syntaxes naturally compose with method chains and ? in particular.

  4. Grepping is easiest when the await keyword is used whether it is postfix, prefix, macro, etc. When a sigil based syntax is used, e.g. #, @, ~, grepping becomes harder. The difference is not great, but .await is slightly easier to grep for than await and await as either of those might be included as words in comments whereas it is more unlikely for .await.

  5. Sigils are likely harder to spot at a glance whereas await is easier to see and especially syntax highlighted. It has been suggested that .await or other postfix syntaxes are harder to spot at a glance or that postfix await offers poor readability. However, in my view, @scottmcm rightly notes that futures of results are common and that .await? helps draw extra attention to itself. Moreover, Scott notes that prefix await leads to garden path sentences and that prefix await in the middle of expressions is no more readable than postfix. With the advent of the ? operator, Rust programmers already need to scan the end of lines to look for control flow.

  6. The precedence of .await and .await() are notable for being completely predictable; they work as their field access and method call counterparts. A postfix macro would presumably have the same precedence as a method call. Meanwhile, prefix await has either a consistent, predictable, and not useful precedence in relation to ? (i.e. await (expr?)), or the precedence is inconsistent and useful (i.e. (await expr)?). A sigil can be given the desired precedence (e.g. taking its intuition from ?).

  7. In 2012, @nikomatsakis noted that Simon Peyton Jones noted once (p. 56) that he is envious of the "power of the dot" and how it provides IDE magic whereby you can narrow the function or field that you mean. Since the power of the dot exists in many popular languages (e.g. Java, C#, C++, ..), this has led to "reaching for the dot" being ingrained in muscle memory. To illustrate how powerful this habit is, here's a screenshot, due to @scottmcm, of Visual Studio with Re#er:
    Re#er intellisense

    C# does not have postfix await. Nonetheless, this is so useful that it is shown in an autocomplete list without being valid syntax. However, it might not occur to many, myself included, to try .aw when .await isn't the surface syntax. Consider the benefit to IDE experience if it were. The await expr and expr await syntaxes do not offer that benefit.

  8. Sigils would likely offer poor familiarity. Prefix await has a benefit from familiarity in C#, JS, etc. However, these do not separate await and ? into distinct operations whereas Rust does. It is also not a stretch to go from await expr to expr.await -- the change is not as radical as going with a sigil. Moreover, due to the aforementioned dot-power, it is likely that .await will be learned by typing expr. and seeing await as the first option in the auto-completion popup.

  9. Sigils are easy to write and would offer good ergonomics. However, while .await is longer to type, it also comes with dot-powers which can make writing flow even better. Not having to break into let statements also facilitates better ergonomics. Prefix await, or worse yet await!(..), lacks in both dot-powers and chaining abilities. When comparing .await, .await(), and .await!(), the first offers to be most terse.

As no one syntax is best at achieving all goals concurrently, a trade-off must be made. For me, the syntax with most benefits and the fewest drawbacks is .await. Notably, it can be chained, preserves await as a keyword, is greppable, has dot-powers and is therefore learnable and ergonomic, has obvious precedence, and finally is readable (especially with good formatting and highlighting).

@Centril Just to clear up a few questions. First, I'd just like to confirm that you only want postfix await, right? Secondly, how to approach the duality of .await being a field access? Would it be ok to have .await as it, or should .await() with the parenthesis be favored so as to imply that some sort of operation is happening there? And thirdly, with .await syntax, would that await be the keyword or just a "field access" identifier?

First, I'd just like to confirm that you only want postfix await, right?

Yep. Right now at least.

Secondly, how to approach the duality of .await being a field access?

It's a slight drawback; there's a major upside tho in the power of the dot. The distinction is something I believe users will learn quickly, especially as it is keyword highlighted and the construct will be used frequently. Moreover, as Scott noted, .await? will be most common which should further alleviate the situation. It should also be easy to add a new keyword entry for await in rustdoc much like we've done for say fn.

Would it be ok to have .await as it, or should .await() with the parenthesis be favored so as to imply that some sort of operation is happening there?

I prefer .await without a tail (); having () at the end seems like unnecessary salt in a majority of cases and would, I conjecture, make users more inclined to look for the method.

And thirdly, with .await syntax, would that await be the keyword or just a "field access" identifier?

await would remain a keyword. Presumably you'd change libsyntax in such a way to not error when encountering await after . and then represent it differently either in AST or when lowering to HIR... but that's mostly an implementation detail.

Thanks for clearing all that up! 👍

At this point the key issue seems to be whether to pursue a postfix syntax. If that's the direction, then surely we can narrow in on which one. Reading the room, most of the supporters of postfix syntax would accept any reasonable postfix syntax over prefix syntax.

Coming into this thread, postfix syntax seemed to be the underdog. However, it has attracted the clear support of four lang team members and openness from a fifth (the other four having been silent so far). Plus, it has attracted considerable support from the wider community commenting here.

Moreover, the supporters of postfix syntax seem to have clear conviction it's the best path for Rust. It appears that some deep problem or convincing rebuttals to the arguments so far advanced would be necessary to turn back this support.

Given this, it seems we need to hear from @withoutboats, who has surely been following this thread closely, and from the other four lang team members. Their views will likely drive this thread. If they have concerns, discussing those would be the priority. If they're convinced of the case for postfix syntax, then we can move on to finding consensus for which one.

Oops, just noticed @Centril had already commented, so I'll hide my post for cleanliness.

@ivandardi Given goal (1) from the post, my understanding is that await would still be a keyword, so would never be a field access, the same way that loop {} is never a struct literal expression. And I would expect it to be highlighted to make the more obvious (like https://github.com/rust-lang/rust-enhanced/issues/333 is waiting to do in Sublime).

My one concern is that making await syntax look like a different access might potentially cause confusion when in a edition 2015 codebase without realizing. (2015 is the default when unspecified, opening up someone else's project, etc.) So long as the 2015 edition has a clear error when there isn't an await field (and warning if there is), I think that (along with Centril's wonderful argument) eliminates my personal worries about a keyword field (or method).

Oh, and one thing that we should also decide while we're at it is how rustfmt would end up formatting it.

let val = await future;
let val = await returns_future();
let res = client.get("https://my_api").await send()?.await json()?;
  1. Uses await - check
  2. First class - check
  3. Chaining - check
  4. Grepping - check
  5. Non sigil - check
  6. Precedence - ¹check
  7. Dot power - ²check
  8. Easy to learn - ³check
  9. Ergonomic - check

¹Precedence: space after await makes it not obvious in deferred position. However it's exactly the same as on the following code: client.get("https://my_api").await_send()?.await_json()?. For all English speakers it's even more natural than with all of the other proposals

²Dot power: it would require additional support for IDE to move await to the left of method call after ., ? or ; is typed next. Not seems to be too hard to implement

³Easy to learn: in prefix position it's already familiar for programmers. In deffered position it would be obvious after that syntax was stabilized


But the strongest point about this syntax is that it will be very consistent:

  • It can't be confused with property access
  • It has obvious precedence with ?
  • It will always have the same formatting
  • It matches human language
  • It don't affected with variable horizontal occurrence in method call chain*

*explaination is hidden under spoiler


With postfix variant it's hard to predict which function is async and where would be the next occurrence of await on horizontal axis:

let res = client.get("https://my_api")
    .very_long_method_name(param, param, param).await?
    .short().await?;

With deferred variant it would be almost always the same:

let res = client.get("https://my_api")
    .await very_long_method_name(param, param, param)?
    .await short()?;

Wouldn't there be a . between the await and the next method call? Like
get().await?.json().

On Wed, Jan 23, 2019, 05:06 I60R <[email protected] wrote:

let val = await future;
let res = client.get("https://my_api").await send()?.await json()?;

  1. Uses await - check
  2. First class - check
  3. Chaining - check
  4. Grepping - check
  5. Non sigil - check
  6. Precedence - ¹check
  7. Dot power - ²check
  8. Easy to learn - ³check
  9. Ergonomic - check

¹Precedence: space makes it not obvious in deferred position. However it's
exactly the same as on the following code: client.get("https://my_api
").await_send()?.await_json()?. For all English speakers it's even more
natural than with all of the other proposals

²Dot power: it would require additional support for IDE to move await to
the left of method call after . or ? is typed next. Not seems to be too
hard to implement

³Easy to learn: in prefix position it's already familiar for programmers,
and in deffered position it would be obvious after that syntax was

stabilized

But the strongest point about this syntax is that it will be very
consistent:

  • It can't be confused with property access
  • It has obvious precedence with ?
  • It will always have the same formatting
  • It matches human language
  • It don't affected with variable horizontal occurrence in method call
    chain*

*explaination is hidden under spoiler

With postfix variant it's hard to predict which function is async and
where would be the next occurrence of await on horizontal axis:

let res = client.get("https://my_api")

.very_long_method_name(param, param, param).await?

.short().await?;

With deferred variant it would be almost always the same:

let res = client.get("https://my_api")

.await very_long_method_name(param, param, param)?

.await short()?;


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456693759,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AIA8Ogyjc8LW6teZnXyzOCo31-0GPohTks5vGAoDgaJpZM4aBlba
.

we should also decide while we're at it is how rustfmt would end up formatting it

That's the domain of https://github.com/rust-dev-tools/fmt-rfcs, so I would say it's off-topic for this isssue. I'm sure there will be something that's consistent with whatever fix-ness and precedence is chosen.

@I60R

let res = client.get("https://my_api").await send()?.await json()?;

How would that look for free functions functions? If I'm reading it right, it's actually a prefix await.

I think this shows that we shouldn't be so quick to discredit the experience of the C# language team. Microsoft puts a lot of thought into usability studies, and we're making a decision here based on a single line of code and the premise that Rust is special enough to have a different syntax from all the other languages. I disagree with that premise -- I mentioned above LINQ and extension methods being pervasive in C#.

However, it might not occur to many, myself included, to try .aw when .await isn't the surface syntax.

~I don't think it will occur to anyone familiar to the other languages.~ That aside, would you expect the completion pop-up to include both await and the Future methods?

[...] and we're making a decision here based on a single line of code and the premise that Rust is special enough to have a different syntax from all the other languages. I disagree with that premise [...]

@lnicola I just want to point out in case you and others have missed it (its easy to miss in the deluge of comments): https://github.com/rust-lang/rust/issues/57640#issuecomment-455846086

Several real examples from actual production code.

Given @Centril's comment above, it seems the postfix options in strongest contention are expr await postfix keyword syntax and expr.await postfix field syntax (and perhaps postfix method syntax isn't out yet).

Compared with postfix keyword, @Centril argues that postfix field benefits from "the power of the dot" -- that IDEs may be better able to suggest it as an autocompletion -- and that instances of its use may be easier to find with grep because the leading dot provides disambiguation from use of the word in comments.

Conversely, @cramertj has argued in favor of postfix keyword syntax on the basis that such syntax makes it clear that it's not a method call or field access. @withoutboats has argued that postfix keyword is the most familiar of the postfix options.

Regarding "the power of the dot", we should consider that an IDE could still offer await as a completion after a dot and then simply remove the dot when the completion is selected. A sufficiently smart IDE (e.g. with RLS) could even notice the Future and offer await after a space.

The postfix field and method options seem to offer the larger surface area for objections, due to the ambiguity, as compared with postfix keyword. As some of the support for postfix field/method over postfix keyword seems to come from a slight unease with the presence of the space in method chaining, we should review and engage with @valff's observation that postfix keyword (foo.bar() await) will look no more surprising than generalized type ascription (foo.bar() : Result<Vec<_>, _>). For both, the chaining continues after this space-separated interlude.

@ivandardi, @lnicola,

I've updated my recent comment with missing example for free function call. If you want more examples you can refer to the last spoiler in my previous comment

In the interest of debating future.await? vs. future await? ...

Something that isn't discussed too much is visual grouping or grepping of a method chain.

consider (match instead of await for syntax highlighting)

post(url).multipart(form).send().match?.error_for_status()?.json().match?

as compared to

post(url).multipart(form).send() match?.error_for_status()?.json() match?

When I visually scan the first chain for await I clearly and quickly identify send().await? and see that we are awaiting the result of the send().

However, when I visually scan the second chain for await, I first snap to seeing await?.error_for_status() and have to go, no, back up, and then connect send() await together.


Yes, some of these same arguments apply for Generalized Type Ascription, but that is not an accepted feature yet. While I like type ascription in a sense I feel that in a _general_ expression context (vague, I know) it ought to be required to be wrapped in parens. That is all off-topic for this discussion though. Also keep in mind that the amount of await in a code base has a high change of being significantly higher than the amount of type ascription.

Generalized Type Ascription also binds the type to the ascribed expression through the use of a distinct operator : . Just like other binary operator, reading it makes it (visually) clear that the two expressions are a single tree. Whereas future await has no operator and could easily be mistaken for future_await out of habbit of rarely seeing two names not separated by an operator except if the first is a keyword (exceptions apply, that is not to say that I prefer prefix syntax, I don't).

Compare this to english if you will, where a hyphen (-) is used to visually group words that would otherwise easily be interpreted as separate.

I think this shows that we shouldn't be so quick to discredit the experience of the C# language team.

I don't think "so quick" and "discredit" are fair here. I think the same thing is happening here that happened in the async/await RFC itself: carefully considering why it was done one way, and figuring out whether the balance comes out in the same way for us. The C# team is even mentioned explicitly in one:

I thought that the idea of await was that you actually wanted the result not the future
so maybe await syntax should be more like 'ref' syntax

let future = task()
but
let await result = task()

so for chaining you should do either

task().chained_method(|future| { /* do something with future */ })

but

task().chained_method(|await result| { /* I've got the result */ })
- foo.await             // NOT a real field
- foo.await()           // NOT a real method
- foo.await!()          // NOT a real macro

They all works well with chaining, and all have cons that's not real field/method/macro.
But since await, as a keyword, is already a special thing, we don't need to make it more special.
We should just select the simplest, foo.await. Both () and !() are redundant here.

@liigo

- foo await        // IS neither field/method/macro, 
                   // and clearly seen as awaited thing. May be easily chained. 
                   // Allow you to easily spot all async spots.

@mehcode

When I visually scan the first chain for await I clearly and quickly identify send().await? and see that we are awaiting the result of the send().

I like spaced version more, since It's much easier to see where future is constructed, and where it is awaited. It's much easier to see what happens, IMHO

image

With dot-separation it looks an awful lot like a method call. Code highlighting won't help a lot, and it's not always available.

Finally, I do believe chaining is not the main use case (except for ?, but foo await? is as clear as possible), and with single await It becomes

post(url).multipart(form).send().match?

vs

post(url).multipart(form).send() match?

Where the latter looks much leaner imo.

So, if we do have narrowed down to future await and future.await, can we just make the magic "dot" optional? So that people can choose which one they want, and most importantly, stop the arguing and move forward!

It is not harmful to have optional syntax, Rust have a lot of such examples. the most well known one is the last sperator (; or , or whatever, after the last item, like in (A,B,)) are mostly optional. I didn't see a strong reason of why we can't make the dot optional.

I think it would be a good time to just do this in Nightly, and let the ecosystem to decide which one is the best for them. We can have lints to enforce the prefered style, makes it customisable.

Then before landing to Stable, we review the community usage and decide whether we should pick one or just leave both.

I completely disagree with the notion that macros wouldn't feel first-class enough for this feature (I wouldn't consider it such a core feature in the first place) and would like to re-highlight my previous comment about await!() (no matter if prefix or postfix) not being a "real macro", but I guess in the end too many people want this feature to be stabilized ASAP, rather than blocking this on postfix macros and prefix await really isn't a good fit for Rust.

In any case, locking this thread for a day has only helped so much and I'm going to unsubscribe now. Feel free to mention me if you are replying directly to one of my points.

My argument against syntax with . is that await is not a field and therefore it shouldn't look like a field. Even with syntax highlighting and bold font it will look like a somehow special field, and even custom formatting wouldn't allow to emphasize that it's not a field because . followed by word is strongly associated with field access.

There's no reason to use syntax with . for better IDE integration either, as we would be able to empower any different syntax with . completion by using something like postfix completion feature available in Intellij IDEA.

We need to perceive await as control flow keyword. If we strongly decide to go with postfix await, then we should consider variant without . and I propose to use the following formatting alongside to better emphasize blocking points with possible interruption:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)
        await?.res.json::<UserResponse>()
        await?.user
        .into();
    Ok(user)
}

More examples

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()) 
    await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)
    await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
    await?.error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()
    await?.error_for_status()?
    .json()
    await?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
    await?.error_for_status()?
    .json()
    await?;

With syntax highlighting

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)
        yield?.res.json::<UserResponse>()
        yield?.user
        .into();
    Ok(user)
}

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()) 
    yield? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)
    yield? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
    yield?.error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()
    yield?.error_for_status()?
    .json()
    yield?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
    yield?.error_for_status()?
    .json()
    yield?;


This also could be answer to @ivandardi point about formatting

I find all these examples for postfix await to be more against it than for it. Long chains of awaits feel wrong, and chaining should be kept to future combinators.

Compare (with the match as await thing for syntax highlighting):

post(url)?.multipart(form).send()?.match?.error_for_status()?.json().match?

to

let value = await post(url)?.multipart(form).send()?.error_for_status()
                 .and_then(|resp| resp.json()) // and then parse JSON

// now handle errors

Notice the lack of awaits in the middle? Because chaining can usually be solved by better API design. If send() returns a custom future with additional methods on it, say, for converting non-200 status messages to hard errors, then there is no need for extra awaits. Right now, in reqwest at least, it seems to run in-place and produce a plain Result rather than a new future, which is where the issue here is coming from.

Chaining awaits is a strong code smell in my opinion, and .await or .await() will confuse so many new users it's not even funny. .await especially feels like something out of Python or PHP, where variable member accesses can magically have special behavior.

await should be up-front an obvious, as if saying, "before continuing, we wait on this" not "and then, we wait on this". The latter is literally the and_then combinator.

Another potential way to think about it is to consider async/await expressions and futures to be like iterators. They are both lazy, can have many states, and can be terminated with syntax (for-in for iterators, await for futures). We wouldn't propose a syntax extension for iterators to chain them together as regular expressions like we are here, right? They use combinators for chaining, and async operations/futures should do the same, only terminating once. It works out rather nice.

Finally, and most importantly, I want to be able to vertically skim through the first few indents of a function and clearly see where async operations happen. My vision isn't that great, and having awaits at the end of the line or stuck in the middle would make things so much more difficult I may just stay with raw futures entirely.

Everybody please read this blog post before commenting.

Async/await is not about convenience. It is not about avoiding combinators.

It is about enabling new things that cannot be done with combinators.

So any suggestions of "let's use combinators instead" are off-topic and should be discussed someplace else.

Notice the lack of awaits in the middle? Because chaining can usually be solved by better API design.

You won't get a pretty chain in real async example. Real example:

req.into_body().concat2().and_then(move |chunk| {
    from_slice::<Update>(chunk.as_ref())
        .into_future()
        .map_err(|_| {
            ...
        })
        .and_then(|update| {
            ...
        })
        .and_then(move |(user, file_id, chat_id, message_id)| {
            do_thing(&file_id)
                .and_then(move |file| {
                    if some_cond {
                        Either::A(do_yet_another-thing.and_then(move |bytes| {
                            ...

                            if another_cond {
                                ...
                                Either::A(
                                    do_other_thing()
                                        .then(move |res| {
                                            ...
                                        }),
                                )
                            } else {
                                Either::B(future::ok(()))
                            }
                        }))
                    } else {
                        Either::B(future::ok(()))
                    }
                })
                .map_err(|e| {
                    ...
                })
        })
        // ...and here we unify both paths
        .map(|_| {
            Response::new(Body::empty())
        })
        .or_else(Ok)
})

Combinators don't help here. Nesting can be removed, but it's not easy (I tried twice and failed). You have tons of Either:A(Either:A(Either:A(...))) at the end.

OTOH it's easily solved with async/await.

@Pauan has written it before I finished my post. So be it. Don't mention combinators, async/await shouldn't look similar. It's fine if it does, there are more important things to consider.


Downvoting my comment won't make combinators more convenient.

Reading through the type ascription comment, I was thinking how that would look combined with await. Given the reservation some people have with .await or .await() as confusing member fields/method access, I wonder if a general "ascription" syntax could be an option, (i.e., "give me what await returns").

Using yield for await for syntax highlighting

let result = connection.fetch("url"):yield?.collect_async():yield?;

Combined with type ascription, e.g.:

let result = connection.fetch("url") : yield?
    .collect_async() : yield Vec<u64>?;

Advantages: still looks nice (IMHO), different syntax from field/method access.
Disadvantages: potential conflicts with type ascription (?), multiple ascriptions theoretically possible:

foo():yield:yield

Edit: It occurred to me that using 2 ascriptions would be more logical in the combined example:

let result = connection.fetch("url") : yield?
    .collect_async() : yield : Vec<u64>?;

@Pauan it would be inappropriate to overapply that blog post. Combinators can be applied in conjunction with await syntax to avoid the problems it describes.

The core problem always was that a spawnable future must be 'static, but if you captured variables by reference in those combinators, you ended up with a future that was not 'static. But awaiting a non-'static future does not make the async fn you are in not 'static. So you can do things like this:

```rust
// slice: &[T]

let x = await future.and_then(|i| if slice.contains(i) { async_fn(slice) });

So, I was planning on not commenting at all, but i wanted to add some notes from someone who doesn't write a lot of asynchronous code, but will still have to read it:

  • Prefix await is a lot easier to notice. This is both when you're expecting it (inside an async fn) or outside (in the expression of some macro body).
  • Postfix await being a keyword might stand out well in chains with little to no other keywords. But as soon as you have closures or similar with other control structures, your awaits will become a lot less noticable and easier to overlook.
  • I find .await as a pseudo field very odd. It's not like any other field in any way that matters to me.
  • I'm unsure if reliance on IDE detection and syntax highlighting is a good idea. There is value in ease of readability when no syntax highlighting is available. Plus with the recent discussions about complicating the grammar and the issues it will cause for tooling, it might not be a good idea to rely on IDEs/editors getting things right.

I don't share the sentiment that await!() as a macro "doesn't feel first-class". Macros are a completely first-class citizen in Rust. They are also not there for "inventing syntax in user code" only. Many "built-in" macros are not user-defined and not there for mere syntactic reasons. format!() is there to give the compiler the power of statically type-checking format strings. include_bytes!() also isn't user-defined and isn't there for mere syntactic reasons, it's a macro because it needs to be a syntax extension as, again, it does special things inside the compiler and it couldn't reasonably be written in any other way without adding it as a core language feature.

And this is kind of my point: macros are a perfect way for introducing and hiding compiler magic in a uniform and unsurprising way. Await is a good example of this: it needs special handling, but its input is just an expression, and as such, it's a great fit for realization using a macro.

So, I don't really see the need for a special keyword at all. That said, if it is going to be a keyword, then it should be just that, a bare keyword – I don't really understand the motivation of disguising it as a field. That just seems wrong, it's not a field access, it's not even remotely like a field access (unlike, say, getter/setter methods with sugar could be), so treating it as one is inconsistent with how fields work today in the language.

@H2CO3 await! macro is good every desirable way except for two properties:

  1. Extra braces are really annoying when you write yet-another-thousand of awaits. Rust removed braces from if/while/... blocks for (I believe) the same reason. It may look unimportant atm, but it's really the case
  2. Asymmetry with async being a keyword. Async should do something bigger than just allow one macro in the method body. Otherwise it could just be #[async] attribute on top of the function. I understand that attribute won't allow you to use it in blocks etc, but it provides some expectation of encountering await keyword. This expectation may be caused by experience in other languages, who knows, but I strongly believe it exist. It may not be addressed, but it has to be considered.

Otherwise it could just be #[async] attribute on top of the function.

For a long time it was, and I'm sorry to see it go. We are introducing another keyword whereas the #[async] attribute worked perfectly fine, and now with attribute-like procedural macros stable, it would even be consistent with the rest of the language both syntactically and semantically, even if it still were known (and handled specially) by the compiler.

@H2CO3 what is the purpose or advantage of having await as a macro from user perspective? Are there any built-in macros that changes programs control flow under the hood?

@I60R The advantage is uniformity with the rest of the language, and thus not having to worry about yet another keyword, with all the baggage that it would bring with itself — e.g. precedence and grouping is obvious in a macro invocation.

I don't see why the second part is relevant – can't there be a built-in macro that does something new? In fact I think pretty much every built-in macro does something "weird" that can't (reasonably/efficiently/etc.) be achieved without compiler support. await!() wouldn't be a new intruder in this regard.

I’ve previously suggested implicit await, and more recently suggested non-chainable await.

The community seems very strongly in favor of explicit await (I’d love for that to change, but…). However, the granularity of explicitness is what creates boilerplate (i.e. needing it many times in the same expression seems to create boilerplate).

[Warning: The following suggestion will be controversial, but please give it a sincere chance]

I’m curious if most of the community would compromise and allow “partial-implicit await”? Maybe reading await once per expression chain is explicit enough?

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

This is something akin to value de-structuring, which I’m calling expression de-structuring.

// Value de-structuring
let (a, b, c) = ...;

// De-sugars to:
let _abc = ...;
let a = _abc.0;
let b = _abc.1;
let c = _abc.2;
// Expression de-structuring
await response = client.get("https://my_api").send()?.json()?;

// De-sugards to:
// Using: prefix await with explicit precedence
// Since send() and json() return impl Future but ? does not expect a Future, de-structure the expression between those sub-expressions.
let response = await (client.get("https://my_api").send());
let response = await (response?.json());
let response = response?;



md5-eeacf588eb86592ac280cf8c372ef434



```rust
// If needed, given that the compiler knows these expressions creating a, b are independent,
// each of the expressions would be de-structured independently.
await (a, b) = (
    client.get("https://my_api_a").send()?.json()?,
    client.get("https://my_api_b").send()?.json()?,
);
// De-sugars to:
// Not using await syntax like the other examples since await can't currently let us do concurrent polling.
let a: impl Future = client.get("https://my_api_a").send();
let b: impl Future = client.get("https://my_api_b").send();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;

let a: impl Future = a?.json();
let b: impl Future = b?.json();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;
let (a, b) = (a?, b?);

I would also be happy with other implementations of the core idea, "reading await once per expression chain is likely explicit enough". Because it reduces boilerplate but also it makes chaining possible with a prefix based syntax which is most familiar to everyone.

@yazaddaruvala There was a warning above so be careful.

I’m curious if most of the community would compromise and allow “partial-implicit await”? Maybe reading await once per expression chain is explicit enough?

  1. I don't think most of community want partial-implicit-anything. Rust native approach is more like "explicit everything". Even casts u8 -> u32 are done expliciy.
  2. It doesn't work well for more complicated scenarios. What should compiler do with Vec<Future>?
await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Should await be performed for every nested call? Ok, we probably can make await work for one line of code (so nested mapFunc won't be awaited), but It's too easy to break this behavior. If await works for one code line then any refactoring like "extract value" breaks it.

@yazaddaruvala

Do I understand you correct in what you're purposing is prefix await that take precedence over ? with the added possibility of using await in the place of the let keyword to changes the context to sprinkle all impl Future<_> returns with await?

Meaning these three statements below are equivalent:

// foo.bar() -> Result<impl Future<_>, _>>
let a = await foo.bar()?;
let a = await (foo.bar()?);
await a = foo.bar()?;

And these two statements below are equivalent:

// foo.bar() -> impl Future<Result<_, _>>
await a = foo.bar()?;
let a = await (foo.bar())?;

I'm not sure the syntax is ideal, but perhaps having a keyword that changes to an "await implicit context" makes the problems of the prefix keyword less of an issue. And at the same time allowing to use more fine grained control over where await is used when needed.

In my opinion, for short one-liners prefix await has the benefit of similarity to both many other programming languages, and many human languages. If in the end it's decided that await should only be a suffix, I expect I'll end up sometimes using an Await!() macro (or whatever similar form doesn't clash with keywords), when I feel that leaning on deeply-ingrained familiarity with English sentence structure will help reduce the mental overhead of reading/writing code. There's definite value in the suffix forms for longer chained expressions, but my explicitly-not-based-on-objective-facts view is that, with the human complexity of prefix await subsidized by spoken language, the simplicity benefit of only having one form does not clearly outweigh the potential code clarity of having both. Assuming you trust the programmer to choose whichever is most appropriate for the current chunk of code, at least.

@Pzixel

I don't think most of community want partial-implicit-anything. Rust native approach is more like "explicit everything".

I don't see it the same way. I see it always being a compromise of explicit and implicit. For example:

  • Types of variables are required at the function scope

    • Types of variables can be implicit within a local scope.

    • Clousure contexts are implicit

    • Given that we can already reason about the local variables.

  • Lifetimes are required to help the borrow checker.

    • But within a local scope they can be inferred.

    • At the function level lifetimes do not need to be explicit, when they are all the same value.

  • I'm sure there are more.

Using await only once per expression/statement, with all the benefits of chaining, is a very reasonable compromise between explicit and reducing boilerplate.

It doesn't work well for more complicated scenarios. What should compiler do with Vec?

await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

I would argue that the compiler should error. There is a huge type difference that cannot be inferred. Meanwhile, the example with Vec<impl Future> is not solved by any of the syntaxes in this thread.

// Given that json() returns a Vec<imple Future>,
// do I use .await on the `Vec`? That seems odd.
// Given that the client.get() returns an `impl Future`,
// do I .await inside the .map? That wont work, given Iterator methods are not `async`
client.get("https://my_api").send().await?.json()[UNCLEAR]?.parse_as::<Vec<String>>()?.map(|x| client.get(x)[UNCLEAR])?;

I'd argue that the Vec<impl Future> is a poor example, or we should hold every proposed syntax to the same standard. Meanwhile, the "partial-implicit await" syntax I proposed works better than the current proposals for more complicated scenarios like await (a, b) = (client.get("a")?, client.get("b")?); using normal postfix or prefix await let (a, b) = (client.get("a") await?, client.get("b") await?); results in sequential network operations, where the partial-implicit version can be run concurrently by the compiler desugarding appropriately (like I've shown in my original post).

Back in 2011, when the async feature in C# was still in testing, I asked about await being a prefix operator and got a response from the C# Language PM. Since there's discussion of whether Rust should use a prefix operator, I thought it might be valuable to post that response here. From Why is 'await' a prefix operator instead of a postfix operator?:

As you can imagine, the syntax of await expressions was a huge point of discussion before we settled on what's now out there :-). However, we didn't consider postfix operators much. There's just something about postfix (cf. HP caluclators and the programming language Forth) that makes things simpler in principle and less approachable or readable in practice. Maybe it's just the way math notation brainwashes us as kids...

We definitely found that the prefix operator (with its literally imperative flavor - "do this!") was by far the most intuitive. Most of our unary operators are already prefix, and await seems to fit with that. Yes it drowns out in complex expressions, but so does evaluation order in general, due to the fact that e.g. function application isn't postfix either. You first evaluate the arguments (which are on the right) and then call the function (which is on the left). Yes there's a difference in that function application syntax in C# comes with parentheses already built in, whereas for await you can usually drop them.

What I see broadly (and have myself adopted) around await is a style that uses a lot of temporary variables. I tend to prefer

var bResult = await A().BAsync();
var dResult = await bResult.C().DAsync();
dResult.E()

or something like that. In general I would usually avoid having more than one await in all but the simplest expressions (i.e. they are all arguments to the same function or operator; those are probably fine) and I would avoid parenthesizing await expressions, preferring using extra locals for both jobs.

Make sense?

Mads Torgersen, C# Language PM


My personal experience as a C# developer is that the syntax drives me to use that style of multiple statements, and that this makes asynchronous code much more awkward to read and write than the synchronous equivalent.

@yazaddaruvala

I don't see it the same way. I see it always being a compromise of explicit and implicit

It's still explicit. I can link a good article on the topic btw.

I would argue that the compiler should error. There is a huge type difference that cannot be inferred. Meanwhile, the example with Vec is not solved by any of the syntaxes in this thread.

Why sould it error? I'm happy having Vec<impl future>, that I could then join together. You propose extra limitations for no reason.

I'd argue that the Vec is a poor example, or we should hold every proposed syntax to the same standard.

We should examine every case. We can't just ignore edge cases because "meh, you know, nobody write this anyway". The language design is primarily about concerning edge cases.

@chescock I agree with you, but being said before, Rust has a very big difference from C# here: in C# you never write (await FooAsync()).Bar(). But in Rust you do. And I'm talking about ?. In C# you have implicit exception that gets propagated through async calls. But in rust you have to be explicit and write ? on function result after it was awaited.

Of course, you can ask users to write

let foo = await bar();
let bar = await foo?.baz();
let bar = bar?;

but it's much weirder than

let foo = await? bar();
let bar = await? foo.baz();

This look much better, but it requires introducing new combination of await and ? doint (await expr)?. Moreover, If we have some other operators, we could write await?&@# and have all combinations work altogether... Looks a bit complicated.

But then we can just place await as a postfix and it will naturally fit in the current language:

let foo = bar() await?;
let bar = foo.baz() await?;

now await? is two separated tokens, but they work altogether. You can have await?&@# and you won't bother about hacking it into the compiler itself.

Of course, it looks a bit weirder than prefix way, but being said, it's all about Rust/C# difference. We may use C# experience to be sure we need an explicit awaiting syntax which is likely to have a separated keyword, but we shouldn't blindly follow the same way and ignore Rust/C# differences.

I was a proponent for prefix await myself for a long time and I even invited C# language devs into the topic (so we have information newer than back from 2011 😄 ), but now I think that postfix await keyword is the best approach.

Don't forget there is a nightly, so we can rework it later if we find better way.

It has occurred to me that we may be able to get the best of both worlds with a combination of:

  • a prefix async keyword
  • a general postfix macro feature

Which together would allow a postfix .await!() macro to be implemented without compiler magic.

Right now, a postfix .await!() macro would require compiler magic. However, if a prefix variant of the await keyword was stabilised, then this would no longer be true: the postfix .await!() macro could be implemented trivially as a macro that prefixed its argument (an expression) with the await keyword and wrapped everything in brackets.

Advantages of this approach:

  • The prefix await keyword is available, and accessible to users familiar with that construct from other languages.
  • The prefix async keyword can be used where it is desirable to make the async nature of an expression "stand out".
  • There would be a postfix variant .await!(), that could be used in chaining situations, or any other situation where the prefix syntax is awkward.
  • There would be no need for (potentially unexpected) compiler magic for the postfix variant (as would otherwise be a problem for the postfix field, method, or macro options).
  • Postfix macros would also be available for other situations (like .or_else!(continue)), which co-incidentally need the postfix macro syntax for similar reasons (they otherwise require the preceding expression to be wrapped in an awkward and unreadable way).
  • The prefix await keyword could be stabilised relatively quickly (allowing the ecosystem to develop) without us having to wait on the implementation of postfix macros. But medium-to-long term we would still leave open the possibility of a postfix syntax for awaiting.

@nicoburns, @H2CO3,

We shouldn't implement control flow operators like await!() and .or_else!(continue) as macros because from a user perspective there's no meaningful reason to do that. Anyway, in both forms users would have exactly the same features and should learn both of them, however if these features would be implemented as macros, additionally users would worry about why exactly they're implemented in this way. It's impossible to not notice that, because there would be just bizarre and artificial difference between regular first-class operators and regular operators implemented as macros (since by itself macros are first-class, but things implemented by them are not).

It's exactly the same answer as for additional . before await: we don't need it. Macros can't imitate first-class control flow: they have different form, they have different use cases, they have different highlighting, they works in different way, and they perceived completely differently.

For both .await!() and .or_else!(continue) features exists a proper and ergonomic solutions with beautiful syntaxes: await keyword and none-coalescing operator. We should prefer implementing them instead of something generic, rarely used, and ugly looking like postfix macros.

There's no reason to use macros, since there's not something complicated like format!(), there's not something rarely used like include_bytes!(), there's not a custom DSL, there's not a removing of duplication in user code, there's not required vararg-like syntax, there's no need in something looking differently, and we can't use this way just because it's possible.

We shouldn't implement control flow operators as macros because from a user perspective there's no meaningful reason to do that.

try!() was implemented as a macro. There was a perfectly fine reason for it.

I have already described what the reason for making await a macro would be – there's no need for a new language element if an existing feature can achieve the goal too.

there's not something complicated like format!()

I beg to differ: format!() is not about complication, it's about compile-time checking. If it wasn't for compile-time checking, it could be a function.

By the way I didn't suggest it should be a postfix macro. (I think it shouldn't.)

For both features exists a proper and ergonomic solutions with beautiful syntaxes

We shouldn't give every single function call, flow control expression, or minor convenience feature its own very special syntax. That only results in a bloated language, full of syntax for no reason, and it's exactly the opposite of "beautiful".

(I have said what I could; I am not going to reply to this aspect of the question any more.)

@nicoburns

It has occurred to me that we may be able to get the best of both worlds with a combination of:

  • a prefix await keyword
  • a general postfix macro feature

This entails making await a contextual keyword as opposed to the real keyword it currently is. I don't think we should do that.

Which together would allow a postfix .await!() macro to be implemented without compiler magic.

This is a more complex solution implementation-wise than foo await or foo.await (especially the latter). There's still "magic", just as much of it; you've just done an accounting manoeuvre.

Advantages of this approach:

  • The prefix await keyword is available, and accessible to users familiar with that construct from other languages.

If we're going to add .await!() later, we're just giving users more to learn (both await foo and foo.await!()) and now users will ask "which should I use when..". This seems to use up more of the complexity budget than foo await or foo.await as single solutions would.

  • Postfix macros would also be available for other situations (like .or_else!(continue)), which co-incidentally need the postfix macro syntax for similar reasons (they otherwise require the preceding expression to be wrapped in an awkward and unreadable way).

I believe postfix macros have value and should be added to the language. That doesn't mean however that they need to be used for awaiting; As you mention, there's .or_else!(continue) and many other places where postfix macros would be useful.

  • The prefix await keyword could be stabilised relatively quickly (allowing the ecosystem to develop) without us having to wait on the implementation of postfix macros. But medium-to-long term we would still leave open the possibility of a postfix syntax for awaiting.

I see no value in "leaving open possibilities"; the value of postfix for composition, IDE experience, etc. is known today. I'm not interested in "stabilize await foo today and hope we can reach consensus on foo.await!() tomorrow".


@H2CO3

try!() was implemented as a macro. There was a perfectly fine reason for it.

try!(..) was deprecated, meaning that we deemed it unfit for the language, in particular because it was not postfix. Using it as an argument - other than for what we ought not to do - seems strange. Moreover, try!(..) is defined without any support from the compiler.

By the way I didn't suggest it should be a postfix macro. (I think it shouldn't.)

await is not "every single .." - in particular, it is not a minor convenience feature; rather, it is a major language feature that is being added.

@phaylon

Plus with the recent discussions about complicating the grammar and the issues it will cause for tooling, it might not be a good idea to rely on IDEs/editors getting things right.

The syntax foo.await is meant to reduce issues for tooling as any editor with any semblance of good support for Rust will already understand the .ident form. What the editor needs to do is just add await to the list of keywords. Moreover, good IDEs already have code completion based on . -- so it seems simpler to extend RLS (or equivalents...) to provide await as the first suggestion when the user types . after my_future.

As for complicating the grammar, .await is actually least likely to complicate the grammar given that supporting .await in the syntax is essentially parsing .$ident and then not erroring on ident == keywords::Await.name().

The syntax foo.await is meant to reduce issues for tooling as any editor with any semblance of good support for Rust will already understand the .ident form. What the editor needs to do is just add await to the list of keywords. Moreover, good IDEs already have code completion based on . -- so it seems simpler to extend RLS (or equivalents...) to provide await as the first suggestion when the user types . after my_future.

I just find this at odds with the grammar discussion. And the issue is not just now, but in 5, 10, 20 years down the line, after a couple editions. But as I hope you noticed, I also mentioend that even if a keyword is highlighted, the await points can go unnoticed if other keywords are involved.

As for complicating the grammar, .await is actually least likely to complicate the grammar given that supporting .await in the syntax is essentially parsing .$ident and then not erroring on ident == keywords::Await.name().

I think that honor belongs to await!(future), as it is already fully supported by the grammar.

@Centril try! eventually became redundant because the ? operator can do strictly more. It's not "unfit for the language". You might not like it, though, which I accept. But for me it's actually one of the best things Rust invented, and was one of the selling points. And I know it's implemented without compiler support – but I fail to see how that's relevant when discussing whether or not it performs control flow. It does, regardless.

await is not "every single .."

But others mentioned here (e.g. or_else) are, and my point still applies to major features anyway. Adding syntax just for the sake of adding syntax is not an advantage, so whenever there is something that already works in a more general case, it should be preferred over inventing new notation. (I know the other argument against macros is that "they're not postfix". I simply don't think that the benefits of await being its own postfix operator are high enough to justify the cost. We have survived nested function calls. We will be equally fine after having written a couple of nested macros.)

On Wed, Jan 23, 2019 at 09:59:36PM +0000, Mazdak Farrokhzad wrote:

  • Postfix macros would also be available for other situations (like .or_else!(continue)), which co-incidentally need the postfix macro syntax for similar reasons (they otherwise require the preceding expression to be wrapped in an awkward and unreadable way).

I believe postfix macros have value and should be added to the language. That doesn't mean however that they need to be used for awaiting; As you mention, there's .or_else!(continue) and many other places where postfix macros would be useful.

The primary reason to use .await!() is that looking like a macro makes
it clear that it can affect control flow.

.await looks like a field access, .await() looks like a function
call, and neither field accesses nor function calls can affect control
flow. .await!() looks like a macro, and macros can affect control
flow.

@joshtriplett I does not affect control flow in the standard sense. This is the basic fact for justifying that the borrow checker should work in futures as defined (and pin is the reasoning how). From the view of the local function execution, executing await is like most other function calls. You continue where you left off, and have a return value on stack.

I just find this at odds with the grammar discussion. And the issue is not just now, but in 5, 10, 20 years down the line, after a couple editions.

I have no idea what you mean by this.

I think that honor belongs to await!(future), as it is already fully supported by the grammar.

I understand, but at this point, due to the numerous other drawbacks this syntax it has, I think it's effectively ruled out.

@Centril try! eventually became redundant because the ? operator can do strictly more. It's not "unfit for the language". You might not _like_ it, though, which I accept.

It is explicitly deprecated and futhermore a hard error to write try!(expr) on Rust 2018. We decided collectively to make it so and thus it was deemed unfit.

The primary reason to use .await!() is that looking like a macro makes it clear that it can affect control flow.

.await looks like a field access, .await() looks like a function call, and neither field accesses nor function calls can affect control flow.

I believe the distinction is something users will learn relatively easily, especially as .await will usually be followed by ? (which is function-local control flow). As for function calls, and as aforementioned, I think it's fair to say that iterator methods are a form of control flow. Moreover, just because a macro can affect control flow doesn't mean it will do so. Many macros do not (e.g. dbg!, format!, ...). The understanding that .await!() or .await will affect control flow (tho in a much weaker sense than ?, as per @HeroicKatora's note) flows from the word await itself.

@Centril

This entails making await a contextual keyword as opposed to the real keyword it currently is. I don't think we should do that.

Eek. That is slightly painful. Perhaps the macro could have a different name like .wait!() or .awaited!() (the latter is quite nice as it makes it clear that is applies to the preceding expression).

"Which together would allow a postfix .await!() macro to be implemented without compiler magic."
This is a more complex solution implementation-wise than foo await or foo.await (especially the latter). There's still "magic", just as much of it; you've just done an accounting manoeuvre.

Moreover, try!(..) is defined without any support from the compiler.

And if we had a prefix await keyword and postfix macros, then .await!() (perhaps with a different name) could also be implemented without compiler support, right? Of course the await keyword itself would still entail a significant amount of compiler magic, but that would simply be applied to the result of a macro, and is no different to try!()'s relationship to the return keyword.

If we're going to add .await!() later, we're just giving users more to learn (both await foo and foo.await!()) and now users will ask "which should I use when..". This seems to use up more of the complexity budget than foo await or foo.await as single solutions would.

I think the complexity this adds to the user is minimal (implementation complexity may be another matter, but if we want postfix macros anyway...). Both forms read intuitively, such that when reading either, it should be obvious what is happening. As for which to choose when writing code, the docs could simply say something like:

"There are two ways to await a Future in Rust: await foo.bar(); and foo.bar().await!(). Which to use is a stylistic preference, and makes no difference to the execution flow"

I see no value in "leaving open possibilities"; the value of postfix for composition, IDE experience, etc. is known today.

That is true. I guess in my mind it is more a matter of making sure that our implementation does not close off the possibility of having a more unified language in future.

I have no idea what you mean by this.

Doesn't matter much. But in general I just also prefer it when syntax is obvious without good highlighting. So things stand out in git diff and and other such contexts.

In theory yes, and for trivial programs yes, but in reality programmers
need to know where their suspend points are.

For a simple example, you could hold a RefCell across a suspend point, and
the behavior of your program will be different than if the RefCell was
released before the suspend point. In large programs there will be
countless subtleties like this, where the fact that the current function
is suspending is important information.

On Wed, Jan 23, 2019 at 2:21 PM HeroicKatora notifications@github.com
wrote:

@joshtriplett https://github.com/joshtriplett I does not affect control
flow in the standard sense. This is the basic fact for justifying that the
borrow checker should work in futures as defined. From the view of the
local function execution, executing await is like any other function call.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba
.

On Wed, Jan 23, 2019 at 02:26:07PM -0800, Mazdak Farrokhzad wrote:

The primary reason to use .await!() is that looking like a macro makes it clear that it can affect control flow.

.await looks like a field access, .await() looks like a function call, and neither field accesses nor function calls can affect control flow.

I believe the distinction is something users will learn relatively easily

Why should they have to?

I believe the distinction is something users will learn relatively easily, especially as .await will usually be followed by ? (which is function-local control flow).

Often yes, but not always, and the syntax should definitely work for cases where this isn't the case.

As for function calls, and as aforementioned, I think it's fair to say that iterator methods are a form of control flow.

Not in the same way that return or ? (or even break) are. It would be very surprising if a function call could return up two levels out of the function that called it (one of the reasons that Rust doesn't have exceptions is precisely because this is surprising), or break out of a loop in the calling function.

Moreover, just because a macro can affect control flow doesn't mean it will do so. Many macros do not (e.g. dbg!, format!, ...). The understanding that .await!() or .await will affect control flow (tho in a much weaker sense than ?, as per @HeroicKatora's note) flows from the word await itself.

I don't like this at all. Effecting control flow is a really important side effect. It should have a different syntactic form to constructs which can't normally do this. Constructs which can do this in Rust are: keywords (return, break, continue), operators (?), and macros (by evaluating to one of the other forms). Why muddy the waters by adding an exception?

@ejmahler I don't see how holding a RefCell is different for the comparison with a normal function call. It could also be that the inner function wants to acquire the same RefCell. But no one seems to have a major issue with not immediately grasping the full potential call graph. Similarly, issues with stack exhaustion receive no special concern that you should have to annotate the provided stack space. Here the comparison would be favorable for coroutines! Should we make normal function calls more visible if they are not inlined? Do we need explicit tail calls to get a hold of this increasingly difficult problem with libraries?

The answer is, imho, that the biggest risk when using RefCell and await is unfamiliarity. When the other above issues can be abstracted away from the hardware, I do believe that we as programmers can also learn not to hold onto RefCell etc. across yield points except where vetted.

On Wed, Jan 23, 2019 at 10:30:10PM +0000, Elliott Mahler wrote:

In theory yes, and for trivial programs yes, but in reality programmers
need to know where their suspend points are.

:+1:

@HeroicKatora

A critical difference is how much code you have to inspect, how many
possibilities you have to account for. Programmers are already used to
making sure that nothing they call also uses a RefCell they have checked
out. But with await, instead of inspecting a single call stack, we have no
control over what gets executed during await - literally any line of the
program might be run. My intuition tell me that the average programmer is
much less equipped to deal with this explosion of possibilities, as they
have to deal with it much less frequently.

For another example of suspend points being critical information, in game
programming, we typically write coroutines that are intended to be resumed
once per frame, and we change game state on each resume. If we overlook a
suspend point in one of these coroutines, we may leave the game in a broken
state for multiple frames.

All of these can be dealt with by saying “be smarter and make fewer
mistakes” of course, but that approach historically does not work.

On Wed, Jan 23, 2019 at 2:44 PM Josh Triplett notifications@github.com
wrote:

On Wed, Jan 23, 2019 at 10:30:10PM +0000, Elliott Mahler wrote:

In theory yes, and for trivial programs yes, but in reality programmers
need to know where their suspend points are.

:+1:


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba
.

For another example of suspend points being critical information, in game programming, we typically write coroutines that are intended to be resumed once per frame, and we change game state on each resume. If we overlook a suspend point in one of these coroutines, we may leave the game in a broken state for multiple frames.

If you drop the key to a SlotMap you leak memory. The language itself does not help you with this inherently. What I want to say is, your examples of how await is vaguely no visible enough (as long as it is a keyword its occurance will be unique) don't seem to generalize to problems that have not occurred for other features. I think you should evaluate await less on what feature the language gives you immediately and more on how it makes it possible for you to express your own features. Evaluate your tools and then apply them. Suggesting a solution for the game state, and how this is addressed good enough: Write a wrapper that asserts on return that the game state has in fact ticked the require amount, async allows you to borrow your own state for that cause. Ban other awaits in that compartmentalized code.

The state explosion in the RefCell comes not from you not checking that it is not used elsewhere but from other code points not knowing that you relied on that invariant and adding it silently. These are addressed the same way, documentation, comments, correct wrapping.

Holding RefCell is not much different than hodling Mutex when doing blocking io. You can get a deadlock, which is worse and harder to debug than a panic. And yet we don't annotate blocking operations with explicit block keyword. :) .

Sharing RefCell between async function will greatly limit their general usability (!Send), so I don't think it will be common. And RefCell should generally be avoided, and when used, borrowed for as short time as possible.

So I don't think hodling RefCell accidentally over a yield-point is a big deal.

If we overlook a suspend point in one of these coroutines, we may leave the game in a broken state for multiple frames.

Coroutines and async functions are a different thing. We're not trying to make yield implicit.

To anyone in favour of macro syntax instead of keyword syntax here: Please describe what it is about await that means it should be a macro and how that's different from, say, while, which _could_ have just been a while! macro (even using just macro_rules!; no special-casing at all).

Edit: This is not being rhetorical. I'm genuinely interested in such a thing, the same way I thought I wanted run-to-first-await (like C#) but the RFC convinced me otherwise.

To anyone in favour of macro syntax instead of keyword syntax here: Please describe what it is about await that means it should be a macro and how that's different from, say, while, which could have just been a while! macro (even using just macro_rules!; no special-casing at all).

I mostly agree with this reasoning, and I do want to use a keyword for prefix syntax.

However, as outlined above (https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831), I'm in favour of also having a postfix macro for the postfix syntax (perhaps .awaited!() to avoid name clashes with the prefix keyword). My reasoning being partly that it keeps things simpler to have a keyword which can only be used in one way, and saving further variations for macros, but being mainly that as far as I can see, nobody has been able to come up with a postfix keyword syntax that I would consider acceptable:

  • foo.bar().await and foo.bar().await() would technically be keywords, but they look like a field access or method call, and I don't think such a control-flow modifying construct should be hidden like this.
  • foo.bar() await is just confusing, especially with further chaining such as foo.bar() await.qux(). I have never seen a keyword used as a postfix like that (I'm sure they exist, but I actually can't think of a single example in any language that I know of keywords that work like this). And I think it reads very confusingly as if the await applies to the qux() not the foo.bar().
  • foo.bar()@await or some other punctuation could work. But it's not particularly nice, and it feels pretty ad-hoc. I actually don't think there are any significant issues with this syntax (unlike the above options). I just feel like once we start adding sigils, then the balance starts to tip towards abandoning custom syntax, and towards it being a macro.

I'll reiterate an earlier idea once more, although tweaked with new thoughts and considerations, then attempt to remain silent.

await as a keyword is only valid within an async function anyway, so we should allow the identifier await elsewhere. If a variable is named await in an outer scope, it cannot be accessed from within an async block unless it is used as a raw identifier. Or, for example, the await! macro.

Furthermore, adapting the keyword like that would allow for my main point: we should allow the mixing and matching of generators and async functions, like so:

// 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
    // same as the current nightly macro.

    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 = await? some_op();

   return Ok(item);
}

I believe this is sufficiently transparent for extremely advanced users who want to do funky stuff, but allows for new and moderate users to use only what is necessary. 2 and 3, if used without any yield statements, are effectively identical.

I also think prefix await? is a great shorthand for adding ? to the result of the async operation, but that's not strictly necessary.

If postfix macros become an official thing (which I kind of hope they will), both await!(...) and .await!() could exist together, allowing for three equivalent ways of doing awaits, for specific use cases and styles. I don't believe this would add any cognitive overhead, but would allow greater flexibility.

Ideally, async fn and #[async] fn* could even share implementation code for transforming the underlying generator into an async state machine.

These issues have made it clear there is no one true preferred style, so the best I can hope for is to provide clean, flexible, readable and easy to approach levels of complexity. I think the above scheme is a good compromise for the future.

await as a keyword is only valid within an async function anyway, so we should allow the identifier await elsewhere.

I don't know if that's practical in Rust, given hygienic macros. If I call foo!(x.await) or foo!(await { x }) when it wants an expr, I think it should be unambiguous that the await keyword is wanted -- not the await field or the await struct literal expression with field init shorthand -- even in a synchronous method.

allowing for three equivalent ways of doing awaits

Please no. (At least in core. Obviously people can make macros in their own code. if they want)

Right now, a postfix .await!() macro would require compiler magic. However, if a prefix variant of the await keyword was stabilised, then this would no longer be true: the postfix .await!() macro could be implemented trivially as a macro that prefixed its argument (an expression) with the await keyword and wrapped everything in brackets.

I'll note that this is equally true -- but easier! -- in the other direction: if we stabilize the .await keyword syntax, people can already make an awaited!() prefix macro if they dislike postfix enough.

I dislike the idea of adding multiple allowed variants (like prefix and postfix) but for a slightly different reason than users will ask. I work in a rather large company and fights over code style are real. I‘d like to only have one obvious and correct form to use. After all, the Zen of python might be right with regards to this point.

I dislike the idea of adding multiple allowed variants (like prefix and postfix) but for a slightly different reason than users will ask. I work in a rather large company and fights over code style are real. I‘d like to only have one obvious and correct form to use. After all, the Zen of python might be right with regards to this point.

I know what you mean. However, there is no way to stop programmers define their own macros doing things like await!(). Simular structs will always be possible. So really there WILL be different variants anyways.

Well, not await!(...) at least since that would be an error; but if a user defines macro_rules! wait { ($e:expr) => { e.await }; } and then uses that as wait!(expr) then will look distinctly unidiomatic and likely fall out of fashion quickly. That significantly decreases the likelihood of variation in the ecosystem and allows users to learn less styles. Thus, I think @yasammez's point is apt.

@Centril If someone want to do bad things they can be hardly stopped. What about _await!() or awai!()?

or, when unicode identifier enabled, something like àwait!().

...

@earthengine The goal is to set community norms (similar to what we do with style lints and rustfmt), not to prevent various people from doing weird things on purpose. We are dealing with probabilities here, not absolute guarantees of not seeing _await!().

Let's summarize and review the arguments for each postfix syntax:

  • __expr await (postfix keyword)__: Postfix keyword syntax uses the await keyword we have already reserved. Await is a magical transformation, and using a keyword helps that stand out appropriately. It doesn't look like a field access or a method or macro call, and we won't have to explain this as an exception. It fits well with current parsing rules and with the ? operator. The space in method chaining is arguably no worse than with Generalized Type Ascription, an RFC that seems likely to be accepted in some form. On the downside, IDEs may need to do more magic to provide an autocompletion for await. People may find the space in method chaining too bothersome (though @withoutboats has argued that might be an advantage), particularly if code is not formatted such that await ends each line. It uses a keyword that perhaps we could avoid using if we took another approach.

  • __expr.await (postfix field)__: Postfix field syntax leverages the "power of the dot" -- it looks and feels natural in chaining and allows IDEs to autocomplete await without performing other operations (such as automatically removing the dot). It's just as concise as postfix keyword. The dot in front of await may make instances of it easier to find with grep. On the downside, it looks like a field access. As an apparent field access, there is no visual cue that "this does something."

  • __expr.await() (postfix method)__: Postfix method syntax is similar to postfix field. On the upside, the () call syntax indicates to the reader, "this does something." Viewed locally, it almost makes sense in the same way that a call to a blocking method would yield execution in a multi-threaded program. On the downside, it's a bit longer and noisier, and disguising await's magic behavior as a method might prove confusing. We might say that await is a method in the same limited sense that call/cc in Scheme is a function. As a method, we'd need to consider whether Future::await(expr) should work consistently with UFCS.

  • __expr.await!() (postfix macro)__: Postfix macro syntax similarly leverages the "power of the dot." The macro ! bang indicates "this might do something magic." On the downside, this is even noisier, and while macros do magic, they don't typically do magic to surrounding code as await does. Also on the downside, assuming we standardize a general-purpose postfix macro syntax, there may be issues with continuing to treat await as a keyword.

  • __expr@, expr#, expr~, and other single-character symbols__: Using a single character as we do with ? maximizes conciseness and perhaps makes postfix syntax seem more natural. As with ?, we may find that we appreciate this conciseness if await starts to pervade our code. On the downside, until and unless the pain of having our code littered with await becomes a problem, it's hard to see a consensus forming around the trade-offs inherent in adopting such a syntax.

I wanted to take a post to say thank you to @traviscross for these summary posts! They've been consistently well-written and well-crosslinked. It's much appreciated. :heart:

I have an idea that adding "pipe operators" like F#, then users can use either prefix or postfix (with explicit syntax difference).

// use `|>` for instance, Rust can choose other sigils if there are conflicts with current syntax
await expr
expr |> await

// and we can use this operator on normal function calls too
f(g(h(x))) 
x |> h |> g |> f
// this is more convenient than "postfix macro"
x.h!().g!().f!()

@traviscross Excellent summary. There's also been some discussion around combining sigil and keyword, e.g. fut@await, so I'll just add this here for people coming to this thread.

I've enlisted the pros and cons of this syntax here. @earthengine says that other sigils than @ are possible, such as ~. @BenoitZugmeyer favors @await, and asks if postfix macros ala expr!await would be a good idea. @dpc argues that @await is too ad-hoc and doesn't integrate well with what we already have, also, that Rust is already sigil-heavy; @cenwangumass agrees that it's too ad-hoc. @newpavlov says that the await part feels redundant, especially if we don't add other similar keywords in the future. @nicoburns says the syntax could work and that there aren't many issues with it, but that it's too ad-hoc of a solution.

@traviscross great summary!

My 0.02$ in the order from worst to best in my opinion:

  • 3 is a defenitely no-go because it isn't even nearly a method call.
  • 2 is not a field, it's very confusing, especially for newcomers. having await on completion list isn't really helpful. After you write tons of them you just write it as fn or extern. Additional completion is even worse than nothing because instead of useful methods/fields I will be suggested by a keyword.
  • 4 Macro is something that fit here, but it doesn't sit well to me. I mean asymmetry with async being keyword, as I mentioned above
  • 5 Sigil may be too terse and harder to spot, but it's a separated entity and may be treated so. Doesn't look similar to anything else therefore producing no confusion for users
  • 1 Best approach IMHO, it's just a sigil that easy to spot and which is already a reserved keyword. Space separation, as mentioned above, is an advantage, not a flaw. It's fine to exist when no formating is done, but with rustfmt it's even less significant.

As someone who's been eagerly waiting for this moment to come, here are my $0.02 as well.

For the most part, I agree with @Pzixel , except on the last two points, in the sense that I'd swap them around, or, maybe, even give both as an option, as with the try!()/? pair. I've still seen people argue in favor of try!() over ? for clarity purposes, and I think having some agency over the look of your code in this particular case is a pro rather than a con, even at the expense of having two different syntaxes.

In particular, an await postfix keyword is great if you write your async code as an explicit sequence of steps, e.g.

let val1 = my_async() await;
...
let val2 = another_async(val1) await;
...
let val3 = yet_another_async(val2) await;

On the other hand, you may prefer to do so something more complicated instead, in the typical Rust-y method chaining style e.g.

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s))~);

I think in this case it is already fairly clear from context that we're dealing with a future, and the verbosity of the await keyboard is redundant. Compare:

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s)) await);

One other thing I like about the single-symbol postfix syntax is that it makes it clear that the symbol belongs to the expression, whereas (to me, personally) the free-standing await looks a bit... lost? :)

I guess what I'm trying to say is that await looks better in statements, whereas a single-symbol postfix looks better in expressions.

These are just my thoughts, anyway.

Since this is already the mother of all bike-shedding threads, I wanted to add another sigil that hasn't been mentioned so far AFAICT: ->.

The idea being that it mirrors the -> from the return type declaration of the function it follows, which - the function being async - is the awaited return type.

async fn send() -> Result<Response, HttpError> {...}
async fn into_json() -> Result<Json, EncodingError> {...}

let body: MyResponse = client.get("http://api").send()->?.into_json()->?;

In the above, what you get from send()-> is a Result<Response, HttpError>, just as written in the function declaration.

These are my $0.02 after reading most of the discussion above and mulling over the proposed options for a few days. There's no reason to give my opinion any weight - I'm just a random person on the internet. I'll probably not comment much further as I'm cautious of not adding more noise to the discussion.


I like a postfix sigil. There's precedence for postfix sigils and I think it would feel consistent with the rest of the language. I don't have any particular preference on what particular sigil is used - none are compelling but I think that will fade with familiarity. A sigil introduces the least noise compared to other postfix options.

I don't mind the idea of replacing . (when chaining) with -> in order to await. It's still concise and unambigious but I'd prefer something that can be used in the same contexts as ?.

I strongly dislike the other postfix options that have been presented. await is neither a field or method and it therefore feels entirely inconsistent with the rest of the language for a control flow construct to be presented as such. There aren't any postfix macros or keywords in the language, so that also seems inconsistent.

If I were new to the language, not knowing it fully, then I would assume that .await or .await() were not special language features and were fields or methods on a type - as is the case with every other field and method in the language that the user will see. Once gaining some more experience, if .await!() was chosen, the user might endeavour to learn how to define their own postfix macros (just as they've learned to define their own prefix macros) - they can't. If that user saw a sigil, they might need to look up documentation (just like ?), but they wouldn't confuse it with anything else and waste time trying to find the definition of .await() or documentation for the field .await.

I like a prefix await { .. }. This approach has clear precedence but does have issues (that have already been discussed in detail). Despite this, I think it would be beneficial for those who prefer using combinators. I wouldn't like this to be the only option implemented, as it isn't ergonomic with method chaining, but I think it would compliment a postfix sigil nicely.

I dislike other prefix options, they don't feel as consistent with other control flow constructs in the language. Similarly to a postfix method, a prefix function is inconsistent, there aren't any other global functions built in to the language used for control flow. There also aren't any macros used for control flow (with the exception of try!(..) but that is deprecated because we've got a better solution - a postfix sigil).


Almost all of my preferences boil down to what feels natural and consistent (to me). Whichever solution is chosen should be given time for experimentation before stabilization - hands-on experience will be a far better judge of which option is best than speculation.

It is also worth considering that there may be a silent majority that could have entirely different opinions - those who engage in these discussions (including myself) aren't necessarily representative of all Rust users (this isn't meant as a slight or to be in any way offensive).

tl;dr Postfix sigils are a natural way to express await (due to precedence) and is a concise, consistent approach. I'd add a prefix await { .. } and a postfix @ (that can be used in the contexts as ?). It's most important to me that Rust remains internally consistent.

@SamuelMoriarty

I think in this case it is already fairly clear from context that we're dealing with a future, and the verbosity of the await keyboard is redundant. Compare:

Sorry, but I didn't even spot ~ at glance look. I reread the whole comment and I my second read was more successful. Makes great explanation why I think sigil is worse. It's just an opinion, but it was just confirmed once more.

Another point against sigils is that Rust become J. For example:

let res: MyResponse = client.get("https://my_api").send()?@?.json()?@?;`

?@? stands for "function that may return an error or a future, that should be awaited, with error, if any, propagated to a caller"

I'd like more to have

let res: MyResponse = client.get("https://my_api").send()? await?.json()? await?;`

@rolandsteiner

Since this is already the mother of all bike-shedding threads, I wanted to add another sigil that hasn't been mentioned so far AFAICT: ->.

Making grammar context-depended doesn't make things better, but only worse. It leads to worse errors and slower compile times.

We have a separate meaning for ->, it has nothing to deal with async/await.

I'd prefer prefix await { .. } and, if possible, a postfix ! sigil.
An exclamation mark would subtly emphasize the laziness of futures. They won't execute until given a command with an exclamation mark.

The above example would then look like this:

let res: MyResponse = client.get("https://my_api").send()?!?.json()?!?;

Sorry if my opinion is not relevant as I've got little async experience and no experience with the Rust's async function ecosystem.

However, looking at .send()?!?.json()?!?; and other combinations like that I understand the basic reasons the sigil-based proposal looks wrong to me.

First, it feels to me that chained sigils quickly becomes unreadable, wherever it's ?!? or ?~? or ?->?. This will be one more thing that beginners stumble on, guessing whether it's one operator or multiple. The information is too tightly packed.

Second, in general, it feels to me that await points are both less common than error propagation points and more significant. The await points are significant enough for me to deserve being a stage in the chain on their own, not a "minor transformation" attached to another stage (and especially not "just one of minor transformations"). I'd probably be fine even with prefix keyword form only (which nearly forces an await to break the chain), but in general that feels too restrictive. My ideal option would probably be an .await!() method-like macro with the future possibility of expanding the macro system to allow method-like user macros.

The minimal baseline for stabilization, in my opinion, is the prefix operator await my_future. Everything else can follow.

expr....await would reflect the context for something going on till await and consistent with rustlang operators. Also async await is a parallel pattern..await cannot be expressed as method or like property

I have my inclination for await!(foo), but since others have pointed out that doing so would force us to unreserve await and thus preclude its future use as an operator until 2021, I'll go with await { foo } as my preference. As for postfix await, I have no particular opinion.

I know it might not be the best place to talk about implicit await, but would something like an explicit implicit await work? So, we first have prefix await landing:

await { future }?

And later we add something similar to

let result = implicit await { client.get("https://my_api").send()?.json()?; }

or

let result = auto await { client.get("https://my_api").send()?.json()?; }

When choosing implicit mode, everything between {} are automatically awaited.

This has unified await syntax and would balance the need for prefix await, chaining, and being as explicit as possible.

I decided to rg --type csharp '[^ ]await' to survey examples of places where prefix was suboptimal. It's possible that not all of these are perfect, but they're real code that went through code review. (Examples slightly sanitized to remove certain domain model things.)

(await response.Content.ReadAsStringAsync()).Should().Be(text);

Using FluentAssertions as a way nicer way to do things than the normal MSTest assert_eq!Assert.Equal.

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

This general thing of "look, I really only needed one thing off that" is a bunch of them.

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

Another "I only needed one property". (Aside: "man am I glad Rust won't need CancellationTokens.)

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

The same .collect() one that people have mentioned in Rust.

foreach (var key in changes.Keys.Intersect((await neededChangesTask).Keys))

I had been thinking about maybe liking postfix keyword, with rustfmt newlines after it (and after a ?, if present), but it's think like these that have me thinking that the newline isn't good in general.

else if (!await container.ExistsAsync())

One of the rare ones where prefix was actually useful.

var response = (HttpWebResponse)await request.GetResponseAsync();

There were a few casts, though of course casts are another place where Rust is postfix but C# is prefix.

using (var response = await this.httpClient.SendAsync(requestMsg))

This one prefix vs postfix doesn't matter, but I think it's another interesting difference: because C# doesn't have Drop, a bunch of things end up _needing_ to go in variables and not chained.

some of @scottmcm 's examples rustified in the various postfix variants:

// keyword
response.content.read_as_string()) await?.should().be(text);
// field
response.content.read_as_string()).await?.should().be(text);
// function
response.content.read_as_string()).await()?.should().be(text);
// macro
response.content.read_as_string()).await!()?.should().be(text);
// sigil
response.content.read_as_string())@?.should().be(text);
// sigil + keyword
response.content.read_as_string())@await?.should().be(text);
// keyword
let previous = branch.list_history(timestamp_utc, None, 1) await?.history_entries.single_or_default();
// field
let previous = branch.list_history(timestamp_utc, None, 1).await?.history_entries.single_or_default();
// function
let previous = branch.list_history(timestamp_utc, None, 1).await()?.history_entries.single_or_default();
// macro
let previous = branch.list_history(timestamp_utc, None, 1).await!()?.history_entries.single_or_default();
// sigil
let previous = branch.list_history(timestamp_utc, None, 1)@?.history_entries.single_or_default();
// sigil + keyword
let previous = branch.list_history(timestamp_utc, None, 1)@await?.history_entries.single_or_default();
// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;
// field
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await?.identity).await?;
// function
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await()?.identity).await()?;
// macro
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await!()?.identity).await!()?;
// sigil
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@?.identity)@?;
// sigil + keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@await?.identity)@await?;

(thanks to @Nemo157 for corrections)

// keyword
let pending = transaction.connection.query(command) await.into_iter().collect::<Vec<EventView>>();
// field
let pending = transaction.connection.query(command).await.into_iter().collect::<Vec<EventView>>();
// function
let pending = transaction.connection.query(command).await().into_iter().collect::<Vec<EventView>>();
// macro
let pending = transaction.connection.query(command).await!().into_iter().collect::<Vec<EventView>>();
// sigil
let pending = transaction.connection.query(command)@.into_iter().collect::<Vec<EventView>>();
// sigil + keyword
let pending = transaction.connection.query(command)@await.into_iter().collect::<Vec<EventView>>();

For me, after reading this, @ sigil is off the table, as it's just invisible especially in front of ?.

I haven’t seen anyone in this thread discuss the await variant for Stream. I know it is out of scope but should we be thinking about it?

It would be a shame if we made a decision that turned out to be a blocker for await on Streams.

// keyword
id = id.or_else(|| self.storage_coordinator.get_default_widget_async() await?.identity);

You can't use await inside a closure like this, there would need to be an additional extension method on Option that takes an async closure and returns a Future itself (at the moment I believe that extension method's signature is impossible to specify, but hopefully we will get some way to make async closures usable at some point).

// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

(or a more direct translation of the code using if let instead of a combinator)

@Nemo157 Yeal, but we can probably do it without extra functions:

id = ok(id).transpose().or_else(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

But if let approach seems be more natural to me indeed.

For me, after reading this, @ sigil is off the table, as it's just invisible especially in front of ?.

Don't forget about alternative code highlighting schemes, for me this code looks like this:
1

Personally I don't think that "invisibility" here is a bigger problem than for standalone ?.

And clever color-scheme can make it even more noticeable (e.g. by using a different color than for ?).

@newpavlov you can't choose color scheme in external tools, e.g. in gitlab/github review tabs.

Being said, it's not a good practice to rely on highlighting only. Others may have other preferences.

Hi guys, I'm a new Rust learner coming from C++. Just want to comment that wouldn't something like

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

or

id = id.or_else_async(async || { 
    self.storage_coordinator.get_default_widget_async() await?.identity 
}) await?;

be basically incomprehensible? The await is pushed towards the very end of the line while our focus is mainly concentrated at the beginning (which is like when you use a search engine).
Something like

id =  await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

or

id = auto await {
    id.or_else_async(async || { self.storage_coordinator.get_default_widget_async()?.identity })
}?;

suggested earlier looks much better to me.

I agree. The first few words are the first thing anyone look at while scanning code, search results, paragraphs of text, etc. This immediately puts any kind of postfix at a disadvantage.

For ? placement, I'm convinced await? foo is sufficiently distinct and easy to learn.

If we stabilize this now and after a year of two of use we decide we really do want something better for chaining, we can consider postfix macros as a general feature.

I would like to propose a variation on the idea of having both pre and postfix await by @nicoburns .
We could have 2 things:

  • a prefix await keyword (for instance, the flavor with stronger binding than ?, but this is less important)
  • a new method on std::Future, for instance fn awaited(self) -> Self::Output { await self }. Its name could also be block_on, or blocking, or something better someone else will come up with.

This would allow both the "simple" prefix usage, and also allow for chaining, while avoiding having to make await a contextual keyword.

Technically the second bullet point can also be accomplished with a postfix macro, in which case we would write .awaited!().

This would allow for code that looks like this:

let done = await delayed;

let value = await delayed_result?;

let value2 = await some.thing()?;

let value3 = some.other().thing().awaited()?;

let value4 = promise
        .awaited()
        .map_err(|e| e.into())?
        .obtain_other_future()
        .awaited();

Other issues aside, the main point of this proposal is to have await as the basic building block of the mechanism, just like match, and then have combinators to save us having to type a lot of keywords and braces of all kinds. I think this entails that they can be taught the same way: first the simple await, then to avoid too many parentheses one can use .awaited() and chain away.


Alternatively, a more magical version could entirely drop the await keyword, and rely on a magic .awaited() method on std::Future, that cannot be implemented by others writing their own futures, but I think this would be fairly counterintuitive and too much of a special case.

a new method on std::Future, for instance fn awaited(self) -> Self::Output { await self }

I'm pretty sure that's impossible (without making the function magic), because to await inside it it would need to be async fn awaited(self) -> Self::Output { await self }, which would still itself need to be awaited. And if we're contemplating making the function magic, it might as well just be the keyword, IMO.

There already exists Future::wait (though apparently 0.3 doesn't have it yet?), which does run a future blocking the thread.

The problem is that _the whole point of await is to _not_ block the thread_. If the syntax to await is prefix and we want a postfix option it must be a postfix macro and not a method.

And if you're going to say use a magical method, just call it .await(), which has already been discussed multiple times in the thread, both as a keyword method and a magic extern "rust-await-magic" "real" fn.

EDIT: scottmcm ninja'd me and GitHub didn't inform me (because mobile?), going to still leave this though.

@scottmcm Would you also be able to survey the frequency count where await seemed OK vs suboptimal? I think a survey for prefix vs postfix frequency count might help answer several questions.

  1. I have the impression that so far the best use case of postfix await has been
client.get("https://my_api").send() await?.json() await?

as many posts use as example. Maybe I missed something, but are there other cases? Wouldn't it be good that you extract this line into a function if it appears frequently all over the codebase?

  1. As I previously discussed, if some people write
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

what they do is exactly visually hiding the await rather than making it explicit so that every yield point can be clearly seen.

  1. Postfix keyword would require inventing something doesn't exist in other mainstream languages. If it doesn't offer significantly better result, inventing it would not be worthwhile.

The await is pushed towards the very end of the line while our focus is mainly concentrated at the beginning (which is like when you use a search engine).

I obviously cannot refute that people scan web content using an F-shaped pattern, @dowchris97.

It's not a given, though, that they use the same pattern for code. For example, this other one on the page sounds like it might better match how people look for ?, and thus might look for await:

Spotted pattern consists of skipping big chunks of text and scanning as if looking for something specific, such as a link, digits, a particular word or a set of words with a distinctive shape (such as an address or signature).

For comparison, let's take the same example if it returned Result instead of impl Future:

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

I think it's pretty clear than an F-shaped reading pattern for code like that doesn't help to find the ?s, nor does it help if it's split into multiple lines like

let id = id.try_or_else(|| {
    let widget = self.storage_coordinator.try_get_default_widget()?;
    Ok(widget.identity)
})?;

I think a description analogous to your could be used to argue that the postfix position is "exactly visually hiding the ? rather than making it explicit so that ever return point can be clearly seen", which I agree was one of the original concerns about ?, but it seems to not have been an issue in practice.

So overall, I think that putting it where people have already been trained to scan for ? is the best way to ensure that people see it. I'd definitely rather they not have to use two different scans simultaneously.

@scottmcm

It's not a given, though, that they use the same pattern for code. For example, this other one on the page sounds like it might better match how people look for ?, and thus might look for await:

Spotted pattern consists of skipping big chunks of text and scanning as if looking for something specific, such as a link, digits, a particular word or a set of words with a distinctive shape (such as an address or signature).

There is also certainly no evidence that using spotted pattern helps understanding code better/faster. Especially when human beings most commonly use F shape pattern, which I'll mention later.

Take this line as an example, when do you start to use spotted pattern suppose you have never read this before.

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

For me, I started when I saw async, then I go to first await?, then self.storage_coordinator.get_default_widget_async(), then .identity, then I finally realized the entire line is asynchronous. I would say this is definitely not the reading experience I like. One reason is that our written language system doesn't have this kind of interleaved forward and backward jump. When you jump, it interrupts building mental model of what this line is doing.

For comparison, what is this line doing, how do you know that?

id = await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

As soon as I reach await?, I immediately had a heads-up that this is asynchronous. I then read let widget = await?, again, without any difficulty, I knew this is async, there is something happening. I feel I follow the F shape pattern. According to https://thenextweb.com/dd/2015/04/10/how-to-design-websites-that-mirror-how-our-eyes-work/, F shape is the most commonly used pattern. So, are we going to design a system that fits human nature or invent some system that needs special education and work against our nature? I prefer the first. The difference would be even stronger when async and normal lines are interleaved like this

await? id.or_else_async(async || {
    let widget1 = await? self.storage_coordinator.get_default_widget_async();
    let result1 = do_some_wierd_computation_on(widget1.identity);
    let widget2 = await? self.network_coordinator.get_default_widget_async();
    let result2 = do_some_strange_computation_on(widget2.identity);
});

Am I supposed to look for await? across lines know?

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

For this, I don't think the comparison is good. First, ? does not change the mental model so much. Second, ?'s success lies in the fact that it does not require you to forward and backward jump between the words. The feeling I have when reading to ? in .try_get_default_widget()? is, "ok, you get some good result out of it". That's it. I don't have to jump back to read something else to understand this line.

So, my overall conclusion is that postfix may provide some limited convenience when writing code. However, it may cause bigger problems for reading code, which I argue is more frequent than writing.

How this would actually look with one proposed rustfmt style and syntax highlighting (while -> async, match -> await):

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

I don't know about you, but I spot the matches instantly.

(Hey @CAD97, I fixed it!)

@dowchris97

If you're going to argue that ? doesn't change the mental model, what's wrong with my argument that await should not change your mental model of the code? (This is why I've favored implicit await options previously, though I've been convinced it's not the right fit for Rust.)

Specifically, you read async at the _beginning_ of the function header. If the function is so big it doesn't fit on one screen, it's almost certainly too big and you've got other, bigger readability problems than finding the await points.

Once you are async, you need to hold that context. But at that point all await is, is a transform that turns a deferred computation Future into the result Output by parking the current train of execution.

It shouldn't matter that this means a complicated state machine transformation. That's an implementation detail. The only difference from OS threads and blocking is that other code may run on the current thread while you're awaiting on the deferred computation, so Sync isn't protecting you as much. (If anything, I'd read that as a requirement for async fn to be Send + Sync instead of inferred and allowed to be thread-unsafe.)

In a much more function-oriented style, your widgets and results would look like (yes I know this isn't using a Monad and real purity etc forgive me):

    let widget1 = await(get_default_widget_async(storage_coordinator(self)));
    let result1 = do_some_wierd_computation_on(identity(widget1));
    let widget2 = await(get_default_widget_async(network_coordinator(self)));
    let result2 = do_some_strange_computation_on(identity(widget2));

But because that's reverse order from the process pipeline, the functional crowd invented the "pipeline" operator, |>:

    let widget1 = self |> storage_coordinator |> get_default_widget_async |> await;
    let result1 = widget1 |> identity |> do_some_wierd_computation_on;
    let widget2 = self |> network_coordinator |> get_default_widget_async |> await;
    let result2 = widget2 |> identity |> do_some_strange_computation_on;

And in Rust, that pipelining operator is ., which provides scoping of what can be pipelined and type-directed lookup through method application:

    let widget1 = self.storage_coordinator.get_default_widget_async().await();
    let result1 = widget1.identity.do_some_wierd_computation_on();
    let widget2 = self.network_coordinator.get_default_widget_async().await;
    let result2 = widget2.identity.do_some_strange_computation_on();

When you think of . as pipelining data the same way that |> does, the longer chains often seen in Rust start to make more sense, and when formatted well (as in Centril's example) you don't miss out on readability because you just have a vertical pipeline of transformations on the data.

await doesn't tell you "hey this is asynchronous". async does. await is just how you park and await the deferred computation, and it completely makes sense to make it available to Rust's pipelining operator.

(Hey @Centril you forgot to make that an async fn (or while fn), which dilutes my point somewhat 😛)

whether or not we could redefine macro invocation

m!(item1, item2)

is same as

item1.m!(item2)

so we can use await both prefix and postfix style

await!(future)

and

future.await!()

@CAD97

await doesn't tell you "hey this is asynchronous". async does. await is just how you park and await the deferred computation, and it completely makes sense to make it available to Rust's pipelining operator.
Yeah, I guess I understand correctly but didn't write rigorously.

I also understand your point. But not convinced, I still think putting await in the front will be tremendously better.

I know |> is used in other langauges to mean something else, but it looks pretty great and extremely clear to me in Rust in place of prefix await:

// A
if |> db.is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match |> db.load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = |> client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()?
    .error_for_status()?;

// D
let mut res: InboxResponse =
    |> client.get(inbox_url)
        .headers(inbox_headers)
        .send()?
        .error_for_status()?
    |> .json()?;

// E
let mut res: Response =
    |> client.post(url)
        .multipart(form)
        .headers(headers.clone())
        .send()?
        .error_for_status()?
    |> .json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = |> self.request(url, Method::GET, None, true)?
               |> .res.json::<UserResponse>()?
                  .user
                  .into();

    Ok(user)
}

The argument about reading order would apply equally well to the ? operator replacing try!(). After all, "hey this might yield" is important, but "hey this might return early" is also important, if not more so. And indeed, concerns about visibility were raised repeatedly in the bikeshed discussion over ? (including this internals thread and this GitHub issue). But the community did eventually settle on approving it, and people got used to it. It would be strange to now end up with the modifiers ? and await appearing on opposite sides of an expression, just because, basically, the community changed its mind on how important visibility is.

The argument about reading order would apply equally well to the ? operator replacing try!(). After all, "hey this might yield" is important, but "hey this might return early" is also important, if not more so. And indeed, concerns about visibility were raised repeatedly in the bikeshed discussion over ? (including this internals thread and this GitHub issue). But the community did eventually settle on approving it, and people got used to it. It would be strange to now end up with the modifiers ? and await appearing on opposite sides of an expression, just because, basically, the community changed its mind on how important visibility is.

It's really not about what your code is doing. It's about your mental model of what your code is doing, whether the model can be easily built or not, whether there are shocks or not, whether it is intuitive or not. These can differ largely from one to another.

I don't want to debate this any further. I think the community is far from settle on a postfix solution although many people here may support it. But I do think there might be a solution:

Mozilla builds Firefox right? It's all about UI/UX! What about a serious HCI research into this problem? So we can really convince each other using data not conjecture.

@dowchris97 There has been (only) two instances of comparing real world code in this thread: One comparison of ~24k lines with databases and reqwest and one comparison that exemplifies why C# is not an accurate comparison to base Rust syntax on. Both turn out that in realworld Rust code, await in postfix seems more natural, and does not suffer from problems that other languages without natural value move semantics have. Unless someone turns up with another decently sized real world example that tends to show the opposite, I'm fairly convinced that prefix syntax is a necessity imposed by other languages upon themselves because they lack the clear value semantics of Rust's pipelining (where reading idiomatic code from left-to-right almost always does precisely what the mental model would suggest).

Edit: Just if it is not clear enough, none of C#, Python, C++, Javascript have member methods that take self by value instead of reference. C++ has the closest with rvalue references but the destructor order is still confusing compared to Rust.

I think the argument that await is better as a prefix stems not from how you have to change your mental model of the code, but more how in rust we have prefix keywords, but don't have postfix keywords (for postfixes, rust uses sigils as is exemplified by ?). That's why await foo() feels easier to read than foo() await and why some people want @ at the end of a statement and dislike having await there.

For a similar reason .await feels strange to use: the dot operator is used to access fields and methods of a struct (which is also why it can't be viewed as a pure pipeline operator), so having .await is like saying "dot is used to access fields and methods of a struct or to access await, which is neither a field or a function".

Personally, I would like to see either prefix await or a postfix sigil (doesn't have to be @).

Both turn out that in realworld Rust code, await in postfix seems more natural

That's a contentious statement. The reqwest example only presents one version of the postfix syntax.

On a different note, if this discussion comes down to a vote of who likes what more, please mention it on reddit so that people don't complain like they did with impl Trait in function arguments.

@eugene2k For the fundamental discussion of whether postfix will fit the mental model of Rust programmers, most or all postfix syntaxes are roughly on the same scale compared to prefix. I don't think there's as much of a significant readability difference between the postfix variants as between prefix and postfix. See also my low-level comparison of operator precedence which concludes that their semantics are equal in most usage, so it's very much a matter of which operator conveys the meaning best (I'd currently prefer an actual function call syntax, but don't have a strong preference over the others either).

@eugene2k Decisions in Rust are never made by voting. Rust is not a democracy, it's a meritocracy.

The core/lang teams look at all the various arguments and perspectives and then decide. This decision is done by consensus (among the team members), not voting.

Although the Rust teams absolutely do take into account the overall desires of the community, ultimately they decide based upon what they think is best for Rust in the long term.

The best way to influence Rust is to present new information, or to make new arguments, or to show new perspectives.

Repeating existing arguments or saying "me too" (or similar) does not increase the chances of a proposal being accepted. Proposals are never accepted based upon popularity.

That also means the various upvotes/downvotes in this thread don't matter at all for which proposal is accepted.

(I'm not referring to you specifically, I'm explaining how things work for the sake of everyone in this thread.)

@Pauan It's been said before that the core/lang teams look at various proposals and then decide. But the decisions of "which is easier to read" are personal decisions. No amount of logical argument presented to the decisionmaker will change their personal view of what they prefer best. Furthermore, the arguments like 'this is how people read search results' and 'a study was conducted by such and such researches showed that people prefer this and that' are easily contested (which is not a bad thing). What may change the mind of the decisionmaker is seeing and disliking the results of the application of their decision in a context they haven't thought of. So, when all these contexts have been looked at and the decisionmakers on the team like one approach, while most other users, who are not on the team like another approach, what should be the final decision?

So, when all these contexts have been looked at and the decisionmakers on the team like one approach, while most other users, who are not on the team like another approach, what should be the final decision?

The decision is always done by the team. Period. That is how the rules were intentionally designed.

And the features are often implemented by team members. And the team members have also established trust within the community. So they have both de jure and de facto authority.

If the situation changes (perhaps based upon feedback) and the team members change their mind, then they can change their decision. But even then, the decision is always done by the team members.

As you say, decisions often involve some amount of subjectivity, and so it's impossible to please everybody, but a decision must be made. In order to reach a decision, the system that Rust uses is based upon team members reaching consensus.

Any discussion about whether Rust should be governed differently is off-topic and should be discussed someplace else.

(P.S. I am not on the core or lang teams, so I don't have any authority in this decision, so I have to defer to them, just the same as you do)

@HeroicKatora

I don't think there's as much of a significant readability difference between the postfix variants

I disagree. I find foo().await()?.bar().await()? is easier to read than foo() await?.bar() await? or even foo()@?.bar()@? Despite that, I feel like having methods that aren't really methods sets a bad precedent.

I'd like to propose another idea. I agree that prefix await isn't easy to chain together with other functions. How about this postfix syntax: foo(){await}?.bar()?{await}? It can't be confused with function calls and seems to me to be easy enough to read in a chain.

And yet another proposal written by me. Let consider the following method call syntax:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[await request(url, Method::GET, None, true)]?
        .res.[await json::<UserResponse>()]?
        .user
        .into();

    Ok(user)
}

What makes it unique among the other proposals:

  • Square brackets makes precedence and scoping much cleaner.
  • Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

I think that extensibility is the strongest advantage here. This syntax would allow to implement a language features that in their common form aren't possible in Rust due to high complexity/usefulness ratio. The list of possible language constructs is provided below:

  1. Deferring of all prefix operators (including await - it's how it supposed to work):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
  1. Pipeline operator functionality:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
  1. Overriding function return:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
  1. Wither functionality:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
  1. Chain splitting:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
  1. Postfix macros:
let x = long().method().[dbg!(it)].chain();

I think introducing a new kind of syntax (magic fields, postfix macros, brackets) has a larger impact on the language than this feature alone, and should require an RFC.

Is there any repository that already heavily uses await? I’d offer to rewrite a larger chunk in every proposed style, so that we can get a better feeling of how these look like and how understandable the code is.

I rewrite in mandatory delimiters:

// A
if await {db.is_trusted_identity(recipient.clone(), message.key.clone())}? {
    info!("recipient: {}", recipient);
}

// B
match await {db.load(message.key)}  {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await { client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
}?.error_for_status()?;

// D
let mut res = await {client.get(inbox_url).headers(inbox_headers).send()}?.error_for_status()?;

let mut res: InboxResponse = await {res.json()}?;

// E
let mut res = await { client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
}?.error_for_status()?;

let res: Response = await {res.json()}?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await {self.request(url, Method::GET, None, true)}?;
    let user = await {res.json::<UserResponse>()}?
        .user
        .into();

    Ok(user)
}

It's almost identical as await!(). So, it looks beautiful! Having used await!() for a year or two, why would you suddenly make up postfix await that appears nowhere in programming language history.

Huh. @I60R's expr.[await it.foo()] syntax with contextual it keyword is pretty neat. I wasn't expecting to like any fancy new syntax proposals, but that is really quite nice, is a clever use of syntax space (because IIRC .[ is not currently valid syntax anywhere), and would solve a lot more problems than just awaiting.

Agreed that it would definitely require an RFC, and it may not turn out to be the best option. But I think it's another point on the side of settling for a prefix syntax for await for the time being, with the knowledge that there are a number of options of solving the problem of "prefix operators are awkward to chain" in a more general way that benefits more than just async/await in future.

And yet another proposal written by me. Let consider the following method call syntax:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[await request(url, Method::GET, None, true)]?
        .res.[await json::<UserResponse>()]?
        .user
        .into();

    Ok(user)
}

What makes it unique among the other proposals:

* Square brackets makes precedence and scoping much cleaner.

* Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

I think that extensibility is the strongest advantage here. This syntax would allow to implement a language features that in their common form are't possible in Rust due to high complexity/usefulness ratio. The list of possible language constructs is provided below:

1. Deferring of all prefix operators (including `await` - it's how it supposed to work):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
1. Pipeline operator functionality:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
1. Overriding function return:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
1. Wither functionality:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
1. Chain splitting:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
1. Postfix macros:
let x = long().method().[dbg!(it)].chain();

Truly incomprehensible.

@tajimaha Looking at your example, I think await {} might actually be much better than await!() if we're going with mandatory delimiters, because it avoids the "too many brackets" problem which can cause readability issues with the await!() syntax.

Compare:
```c#
await {foo.bar(url, false, qux.clone())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(p.s. You can get syntax highlighting of async and await for simple examples by setting the the language to c#.)

@nicoburns You can use any of (), {}, or [] with the macro.

@sgrif That's a good point. And as this is the case, I would suggest that "prefix keyword with mandatory delimeters" makes very little sense as an option. As it breaks consistency with other macros for basically no benefit.

(FWIW, I still think that "prefix without delimeters" with a general solution like postfix macros or @I60R's suggestion for postfix makes most sense. But the "just stick with the existing macro"option is growing on me...)

I would suggest that "prefix keyword with mandatory delimeters" makes very little sense as an option. As it breaks consistency with other macros for basically no benefit.

Why does it make little sense and why a keyword not having the same syntax as a macro is a problem?

@tajimaha

I would suggest that "prefix keyword with mandatory delimeters" makes very little sense as an option. As it breaks consistency with other macros for basically no benefit.

Why does it make little sense and why a keyword not having the same syntax as a macro is a problem?

Well if await were a macro, that would have the advantage of not adding any extra syntax to the language. Thus reducing the complexity of the language. However, there is a good argument for using a keyword: return, break, continue, and other control-flow modifying constructs are also keywords. But these all work without delimiters, so in order to be consistent with these constructs, await would need to work without delimiters too.

If you have await-with-mandatory delimiters then you have:

// Macros using `macro!(foo);` syntax 
format!("{}", foo);
println!("hello world");

// Normal keywords using `keyword foo;`
continue foo;
return foo;

// *and* the await keyword which is kind of in between the other two syntaxes:
await(foo);
await{foo};

This is potentially confusing as you now have to remember 3 syntax forms instead of 2. And seeing as keyword-with-mandatory-delimiters doesn't offer any benefits over the macro syntax, I think it would be preferable just to stick with the standard macro syntax if we wish to enforce delimiters (which I'm not at all convinced that we should).

A question for those who are using lots of async/await in Rust today: how frequently are you awaiting functions/methods vs variables/field?

Context:

I know that in C# it's common to do things that boil down to this pattern:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

That way both run in parallel. (Some will say that Task.WhenAll should be used here, but the perf difference is tiny, and it makes the code messier since it requires going through array indexes.)

But it's my understanding that, in Rust, that will not actually run in parallel at all, since the poll for fooTask wouldn't get called until bar got its value, and it _needs_ to use a combinator, perhaps

let (foo, bar) = when_all!(
    self.foo_async(),
    self.bar_async(),
).await;

So given that, I'm curious whether one regularly ends up having the future in a variable or field that you need to await, or if one is almost always awaiting call expressions. Because if it's the latter, there's a tiny formatting variant of postfix keyword that we haven't really discussed: postfix keyword _no-space_.

I haven't thought much about whether it's any good, but it would be possible to write code as just

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

(I don't actually want to have a rustfmt discussion, as I've said, but I recall that one of the reasons for disliking postfix keyword _was_ the space breaking up visual chunking.)

If we go with that, might as well go with the .await syntax to leverage
the power of the dot, no?

A question for those who are using lots of async/await in Rust today: how frequently are you awaiting functions/methods vs variables/field?

But it's my understanding that, in Rust, that will not actually run in parallel at all [...]

Correct. From the same code-base as before, here is this example:

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(move |id| {
        self__.request(URL, Method::GET, vec![("info".into(), id.into())])
            .and_then(|mut response| response.json().from_err())
    });

    await!(futures_unordered(futures).collect())?
};

If I were to rewrite the closure with an async closure:

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(async move |id| {
        let mut res =
            await!(self__.request(URL, Method::GET, vec![("info".into(), id.into())]))?;

        Ok(await!(res.json())?)
    });

    await!(futures_unordered(futures).collect())?
};

If I were to switch to the .await syntax (and chain it together):

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Is there any repository that already heavily uses await? I’d offer to rewrite a larger chunk in every proposed style

@gralpli Sadly, nothing I can open source heavily uses await!. It definitely lends itself more to application code at the moment (especially being so unstable).

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

These lines are exactly showing how code is messed up by overusing postfix and chaining.

Let's see the prefix version:

let func = async move |id| {
    let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
    Ok(await(req.json())?)
}
let responses: Vec<Response> = await {
    futures_unordered(all_ids.into_iter().map(func)).collect()
}?;

The two versions both use 7 lines but IMO the second one is cleaner. There are also two takeaways for using mandatory delimiters:

  1. The await { future }? doesn't look noisy if future part is long. See let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. When line is short, using await(future) could be better. See Ok(await(req.json())?)

IMO, by switching between the two variants, this code's readability is much better than before.

The first example is badly formatted. I don't think rustfmt formats it like
that. Could you please run rustfmt on it and post it again here?

@ivandardi @mehcode Could you do that? I don't know how I can format .await syntax. I just copied the code. Thanks!

I'd like to add that this example shows:

  1. Production code won't just be nice and simple chains like:
client.get("https://my_api").send().await?.json().await?
  1. People can overuse or misuse chaining.
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Here, the async closure handles every ID, it has nothing to do with higher level control futures_unordered. Putting them together significantly reduces your ability to understand it.

Everything _was_ run through rustfmt from my post (with some minor alterations to make it compile). Where the .await? is placed is not decided yet and currently I place it at the end of the line being awaited.


Now I agree that it all looks pretty awful. This is code written on a deadline and stuff is bound to look awful when you have a room of cooks cranking to get something out.

I do want to point out (from your point) that abusing prefix can look far worse (in my opinion of course):

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

Now let's have fun and make it far nicer using hindsight and some new adapters in futures v0.3

Prefix with "Obvious" Precedence (and sugar)
let responses: Vec<Response> = await? stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect();
Prefix with "Useful" Precedence
let responses: Vec<Response> = await stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect()?;
Prefix with Mandatory Delimiters
let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;
Postfix Field
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect().await?;
Postfix Keyword
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect() await?;

Minor nit here. There is no TryStreamExt::and_then, there probably should be. Sounds like an easy PR for anyone with time that wants to contribute.


  • I want to, again, express my strong dislike for await? In long chains I completely lose track of the ? which I have grown to look for at the end of expressions to signify that this expression is fallible and may _exit the function_.

  • I further want to express my growing dislike for await .... ? (useful precedence) as consider what would happen if we have a fn foo() -> Result<impl Future<Output = Result<_>>>

    // Is this an error? Does`await .. ?` bind outer-most to inner?
    await foo()??
    

I do want to point out (from your point) that abusing prefix can look far worse (in my opinion of course):

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

This isn't actually a concern because in python javascript, people are more likely to write them in separate lines. I haven't actually see await (await f) in python.

Prefix with Mandatory Delimiters

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

This also seems to be back to using combinators. While the point of introducing async / await is to reduce its usage where appropriate.

Well, that's the whole point of having postfix await. Since this is Rust, people are less likely to write them in separate lines, since Rust encourages chaining. And for that, postfix syntax is essentially mandatory so that the flow of instructions follows the same flow of line-reading. If we don't have postfix syntax, then there will be a lot of code with temporaries that are also chained, whereas if we had postfix await that could all be reduced to a single chain.

@ivandardi @mehcode Copying from Rust RFC on async/await:

After gaining experience & user feedback with the futures-based ecosystem, we discovered certain ergonomics challenges. Using state which needs to be shared across await points was extremely unergonomic - requiring either Arcs or join chaining - and while combinators were often more ergonomic than manually writing a future, they still often led to messy sets of nested and chained callbacks.

... use with a syntactic sugar which has become common in many languages with async IO - the async and await keywords.

From a user's perspective, they can use async/await as if it were synchronous code, and only need to annotate their functions and calls.

So, the whole point of introducing async await is reducing chaining and making async code as if they are sync. Chaining is only mentioned twice in this RFC, "requiring either Arcs or join chaining" and "still often led to messy sets of nested and chained callbacks". Doesn't sounds too positive to me.

Arguing for chaining and thus postfix keyword would possibly need a major rewrite of this RFC.

@tajimaha You misunderstand the RFC. It is talking specifically about the Future combinators (map, and_then, etc.), it is not talking about chaining in general (e.g. methods which return impl Future).

I think it's safe to say that async methods will be quite common, so chaining is indeed quite important.

In addition, you misunderstand the process: the RFC does not need to be rewritten. The RFC is a starting point, but it's not a specification. None of the RFCs are set in stone (nor should they be!)

The RFC process is fluid, it isn't nearly as rigid as you are implying. Nothing in the RFC prevents us from discussing postfix await.

Also, any changes will be put into the stabilization RFC, not the original RFC (which has already been accepted, and thus won't be changed).

Arguing for chaining and thus postfix keyword would possibly need a major rewrite of this RFC.

I'm joking here.

One thing is probably true, when writing the RFC. People specifically wanted a new tool "that can use async/await as if it were synchronous code". Tradition following syntax would better fulfill this promise.
And here you see, it's not future combinators,

let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Yet, "they still often led to messy sets of nested and chained callbacks."

People specifically wanted a new tool "that can use async/await as if it were synchronous code". Tradition following syntax would better fulfill this promise.

I don't see how that's true: both prefix and postfix await fulfill that desire.

In fact, postfix await probably fulfills that desire better, because it's very natural with method chains (which are very common in synchronous Rust code!)

Using prefix await heavily encourages a lot of temporary variables, which is often not idiomatic Rust style.

Yet, "they still often led to messy sets of nested and chained callbacks."

I see exactly one closure, and it's not a callback, it's just calling map on an Iterator (nothing at all to do with Futures!)

Please don't try to twist around the words of the RFC to justify prefix await.

Using the RFC to justify prefix await is very strange, because the RFC itself says that the syntax is not final and will be decided later. The time for that decision is now.

The decision will be made based upon the merits of the various proposals, the original RFC is completely irrelevant (except as a useful historical reference).

Note, @mehcode’s latest example is mostly using _stream_ combinators (and the one future combinator could trivially be replaced with an async block). This is the equivalent of using iterator combinators in synchronous code, so can be used in some situations when they’re more appropriate than loops.

This is offtopic, but most of the conversation here is being had by a dozen or so commenters. Of the 383 comments at the time I scraped this issue, there were only 88 unique posters. In an effort to avoid burning out/over burdening anyone who would have to go and read these comments, I would recommend being as thorough in your comments as possible and making sure that it is not a reiteration of a previous point.


Histogram of the comments

HeroicKatora:(32)********************************
Centril:(22)**********************
ivandardi:(21)*********************
I60R:(21)*********************
Pzixel:(16)****************
novacrazy:(15)***************
scottmcm:(13)*************
EyeOfPython:(11)***********
mehcode:(11)***********
Pauan:(10)**********
XX:(9)*********
nicoburns:(9)*********
tajimaha:(9)*********
skade:(8)********
CAD97:(8)********
Laaas:(8)********
dpc:(8)********
ejmahler:(7)*******
Nemo157:(7)*******
yazaddaruvala:(6)******
traviscross:(6)******
CryZe:(6)******
Matthias247:(5)*****
dowchris97:(5)*****
rolandsteiner:(5)*****
earthengine:(5)*****
H2CO3:(5)*****
eugene2k:(5)*****
jplatte:(4)****
lnicola:(4)****
andreytkachenko:(4)****
cenwangumass:(4)****
richardanaya:(4)****
chpio:(3)***
joshtriplett:(3)***
phaylon:(3)***
phaazon:(3)***
ben0x539:(2)**
newpavlov:(2)**
comex:(2)**
DDOtten:(2)**
withoutboats:(2)**
valff:(2)**
darkwater:(2)**
tanriol:(1)*
liigo:(1)*
yasammez:(1)*
mitsuhiko:(1)*
mokeyish:(1)*
unraised:(1)*
mzji:(1)*
swfsql:(1)*
spacekookie:(1)*
sgrif:(1)*
nikonthethird:(1)*
edwin-durai:(1)*
norcalli:(1)*
quodlibetor:(1)*
chescock:(1)*
BenoitZugmeyer:(1)*
F001:(1)*
FuGangqiang:(1)*
Keruspe:(1)*
LegNeato:(1)*
MSleepyPanda:(1)*
SamuelMoriarty:(1)*
Swoorup:(1)*
Uristqwerty:(1)*
alexmaco:(1)*
arabidopsis:(1)*
arielb1:(1)*
axelf4:(1)*
casey:(1)*
lholden:(1)*
cramertj:(1)*
crlf0710:(1)*
davidtwco:(1)*
dyxushuai:(1)*
eaglgenes101:(1)*
AaronFriel:(1)*
gralpli:(1)*
huxi:(1)*
ian-p-cooke:(1)*
jonimake:(1)*
josalhor:(1)*
jsdw:(1)*
kjetilkjeka:(1)*
kvinwang:(1)*

Note, @mehcode’s latest example is mostly using _stream_ combinators (and the one future combinator could trivially be replaced with an async block). This is the equivalent of using iterator combinators in synchronous code, so can be used in some situations when they’re more appropriate than loops.

Same can be argued here that I can / should use prefix await where they are more appropriate than chaining.

@Pauan Apparently it's not just twisting words. I'm showing an actual code problem, written by a postfix syntax supporter. And as i said, prefix style code illustrates your intention better while doesn't necessarily have a lot of temporaries as postfix supporters complain (at least in this case). also, suppose your code has a one liner chain with two awaits, how can I debug the first one? (this is a true question and i don't know).
Second, rust community is becoming larger, people from diverse background (like me, i use python/c/java the most) won't all agree on method chains are the best ways to do things. i hope when making decision, it's not (shoudn't be ) just based on the earliest adopters' point of view.

@tajimaha The biggest clarity change from post-fix to prefix seems to be using a local closure to remove some nested function arguments. This does not appear unique to prefix to me, those are fairly orthogonal. You can do the same for postfix, and I think its even clearer. I agree this maybe a misuse of chaining for some code bases but I don't see how this misuse is unique or connected to postfix in a major way.

let get_one_id = async move |id| {
    self.request(URL, Method::GET, vec![("info".into(), id.into())])
        .await?
        .json().await
};

let responses: Vec<Response> = futures_unordered(all_ids.into_iter().map(get_one_id))
    .collect().await?;

But, in postfix the let-binding and the Ok on the result can be removed together the last ? to directly supply the result and then the code block is also unecessary depending on personal taste. This does not work nicely in prefix at all due to two awaits in the same statement.

I don't grasp the regularly stated sentiment that let bindings are unidiomatic in Rust code. They are pretty frequent and common in code examples, especially around result handling. I rarely see more then 2 ? in code that I deal with.

Also, what idiomatic is changes over the lifetime of a language, so I would take great care with using it as an argument.

I don’t know if something like this has been suggested before, but could a prefix await keyword apply to an entire expression? Taking the example that has been brought up before:

let result = await client.get("url").send()?.json()?

where get, send, and json are async.

For me (having little async experience in other programming languages) the postfix expr await looks natural: “Do this, _then_ wait for the result.”

There were some concerns that the following examples look weird:

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

However, I'd argue that this should be split into several lines:

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

This is much clearer and has the additional advantage that the await is easy to spot, if it is always at the end of the line.

In an IDE, this syntax lacks the "power of the dot", but is still better than the prefix version: When you type the dot and then notice that you need to await, you just have to delete the dot and type "await". That is, if the IDE doesn't offer autocomplete for keywords.

The dot syntax expr.await is confusing because no other control-flow keyword uses a dot.

I think the problem is although we have chaining, which can be pretty sometimes, we should not go to the extreme saying that everything should be done in chaining. We should provide tools also for C or Python style programming. Although Python almost has no chaining component there, its code is often praised to be readable. Python programmers also don’t complain we have too many temp variables.

How about a postfix then ?

I don't grasp the regularly stated sentiment that let bindings are unidiomatic in Rust code. They are pretty frequent and common in code examples, especially around result handling. I rarely see more then 2 ? in code that I deal with.

Also, what idiomatic is changes over the lifetime of a language, so I would take great care with using it as an argument.

This inspired me to survey current Rust code where there are two or more ? in one line (may be someone can survey multiline usage). I surveyed xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, yew, Rocket, exa, iron, parity-ethereum, tikv. These are Rust projects with most stars.

What I find is that roughly only about 40 lines out of 585562 total lines use two or more ? in one line. That is 0.006%.

I also want to point out studying existing code use patterns won't reveal user experience of writing new code.

Suppose you are given a job to interact with a new API or you are new at using requests. Is it likely you will write

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

in just one shot? If you are new to the API, I doubt you will want to make sure you construct the request correctly, see the return status, verify your assumption about what this API returns or just play with the API like this:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

A network API is nothing like in memory data, you don't know what's there. This is pretty natural for prototyping. And, when you are prototyping, you worry about everything gets right NOT too many temp variables. You can say we can use postfix syntax like:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = request.send().await?;
dbg!(response);
let data = response.json().await?;
dbg!(data);

But, if you already have this:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

All you have to do is probably wrap it in a function and your job is done, chaining doesn't even emerge in this process.

What I find is that roughly only about 40 lines out of 585562 total lines use two or more ? in one line.

I'd like to suggest that this isn't a helpful measurement. What really matters is more than one control flow operator _per expression_. Per the typical (rustfmt) style, they will almost always end up on different lines in the file though they belong to the same expression, and would thus be chained in the amount that postfix (theoretically) matters for await.

we should not go to the extreme saying that everything should be done in chaining

Has anyone said that everything _should_ be done with chaining? All I've seen so far is that it should be _ergonomic_ to chain in cases where it makes sense, the same as happens in synchronous code.

only about 40 lines out of 585562 total lines use two or more ? in one line.

I'm not sure that's relevant to prefix vs postfix. I'll note that _none_ of my C# examples of wanting postfix included multiple awaits in a line, nor even multiple awaits in a statement. And @Centril's example of potential postfix layout didn't put multiple awaits on a line either.

A better comparison might be to things chained off ?, like these examples from the compiler:

Ok(&self.get_bytes(cx, ptr, size_with_null)?[..size])
self.try_to_scalar()?.to_ptr().ok()
let idx = decoder.read_u32()? as usize;
.extend(self.at(cause, param_env).eq(v1, v2)?.into_obligations());
for line in BufReader::new(File::open(path)?).lines() {

Edit: Looks like you beat me this time, @CAD97 :slightly_smiling_face:

This is shockingly similar to javascipt's promise code with a lot of thens. I wouldn't call this synchronous. It's almost surely chaining happen to have an await and pretend to be synchronous.

Prefix with Mandatory Delimiters

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

@CAD97 @scottmcm Good point. I did know there is limitation to what I'm measuring:

(may be someone can survey multiline usage)

I'm doing this because @skade mentioned similarity between await and ?, so I did a quick analysis. I'm providing an idea not doing a serious research. I think a finer-grained analysis would be hard to do by looking at the code, right? It may need parsing and identifying expressions, which I'm not familiar with. I hope someone can do this analysis.

Has anyone said that everything should be done with chaining? All I've seen so far is that it should be ergonomic to chain in cases where it makes sense, the same as happens in synchronous code.

What I'm saying is if only postfix is added, it would be unergonomic when C/Python style looks good (of course imo). I also address chaining may not be needed when you prototype.

I have the feeling the direction of this thread is far too much focused on excessive chaining, and how to make await code as concise as possible.

I want to encourage everyone instead to look at how async code is different from the synchronous variants, and how this will influence usage, and resource utilization. Less than 5 comments in the 400 in this thread mention those differences. If you are not aware about these differences, grab the current nightly version, and try to write a decent chunk of async code. Including try to get an idiomatic (non-combinator) async/await version of the piece of code compiling that is discussed about in the last 20 posts.

It will make a difference whether things exist between purely between yield/await points, whether references are persisted across await points, and often some peculiar requirements around futures make it not possible to write code as concise as imagined in this thread. E.g. we can't put arbitrary async functions in arbitrary combinators, because those might not work with !Unpin types. If we create futures from async blocks they might not be directly compatibly with combinators like join! or select!, because they need pinned and fused types, so additional calls to pin_mut! and .fuse() might be required within.

Additionally for working with async blocks the new macro based utilities work join! and select! work far better than the old combinator variants. And these are in the excessive fashion that is here often provided as examples

I don't know how postfix await can work with .unwrap() in this simplest tokio example

let response = await!({
    client.get(uri)
        .timeout(Duration::from_secs(10))
}).unwrap();

If prefix is adopted, it will become

let response = await {
    client.get(uri).timeout(Duration::from_secs(10))
}.unwrap();

But if postfix is adopted,

client.get(uri).timeout(Duration::from_secs(10)).await.unwrap()
client.get(uri).timeout(Duration::from_secs(10)) await.unwrap()

Is there any intuitive explanation we can give users? It conflict with existing rules. await is a field? or await is binding that has a method called unwrap()? TOO BAD! We unwrap a lot when we start a project. Violated multiple design rules in The Zen of Python.

Special cases aren’t special enough to break the rules.
If the implementation is hard to explain, it's a bad idea.
In the face of ambiguity, refuse the temptation to guess.

Is there any intuitive explanation we can give users? It conflict with existing rules. await is a field? or await is binding that has a method called unwrap()? TOO BAD! We unwrap a lot when we start a project. Violated multiple design rules in The Zen of Python.

I would say, although there are too many documents in docs.rs are calling unwrap, unwrap should be replaced with? in many real world cases. At lease, this is my pratice.

What I find is that roughly only about 40 lines out of 585562 total lines use two or more ? in one line.

I'd like to suggest that this isn't a helpful measurement. What really matters is more than one control flow operator _per expression_. Per the typical (rustfmt) style, they will almost always end up on different lines in the file though they belong to the same expression, and would thus be chained in the amount that postfix (theoretically) matters for await.

I do disclaim there can be limit for this approach.

I surveyed again for xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, yew, Rocket, exa, iron, parity-ethereum, tikv. These are Rust projects with most stars. This time I looked for pattern:

xxx
  .f1()
  .f2()
  .f3()
  ...

and whether there are multiple control flow operators in these expressions.

I identified ONLY 15 out of 7066 chains have multiple control flow operators. That's 0.2%. These lines span 167 out of 585562 lines of code. That's 0.03%.

@cenwangumass Thanks for taking the time and quantifying. :heart:

One consideration is that since Rust has variable rebinding with let, it may make a compelling argument for prefix await, in that if used consistently, you would have a separate line of code for each await await-point. The advantage being twofold: stacktraces, as the line number gives greater context as to where the problem occurred, and debugging breakpoint ease, as it's common to wish to set a breakpoint on each separate await-point to inspect variables, may outweigh the brevity of a single line of code.

Personally I am torn between prefix-style and postfix sigil though after reading https://github.com/rust-lang/rust/issues/57640#issuecomment-457457727 I am probably mostly in favor of a postfix sigil.

Rendered prefix style:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = await? self.request(url, Method::GET, None, true));
    let user = await? user.res.json::<UserResponse>();
    let user = user.user.into();

    Ok(user)
}

Rendered postfix style:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)) await?;
    let user = user.res.json::<UserResponse>() await?;
    let user = user.user.into();

    Ok(user)
}

Rendered postfix sigil @:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true))@?;
    let user = user.res.json::<UserResponse>()@?;
    let user = user.user.into();

    Ok(user)
}

Rendered postfix sigil #:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true))#?;
    let user = user.res.json::<UserResponse>()#?;
    let user = user.user.into();

    Ok(user)
}

I haven't seen enough people talk about await for Streams. While it is out of scope, making the decision around await with a little bit of foresight from Stream might be worth the effort.

We will likely need something like these:
Prefix syntax

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

Postfix syntax

for response in stream await {
    let response = response?;
    ...
}
for response in stream.await!() {
    let response = response?;
    ...
}

// Or a specialized variant of `await` and `?`
//     Note (Not Obvious): The `?` actually applies to each response of `await`
for response in stream await? {
    ...
}
for response in stream.await!()? {
    ...
}

Possibly most ergonomic/consistent syntax

let results: Vec<Result<_, _>> = ...;
for value in? results {
    ...
}
for response await? stream {
    ...
}

I just want to make sure these examples are discussed at least a little, because while postfix is nice for chaining Future, at first glance it does seem the least intuitive for Stream. I'm not sure what the right solution here is. Maybe postfix await for Future and a different style of await for Stream? But for different syntaxes we would need to ensure the parsing is non-ambiguous.

These are all just my initial thoughts, it might be worth giving the Stream usecase some more thought from a postfix perspective. Anyone have any thoughts around how postfix might work well with Stream, or if we should have two syntaxes?

Syntax for stream iteration is not something that's going to happen for quite some time I'd imagine. It's fairly easy to use a while let loop and do it manually though:

Postfix Field
while let Some(value) = stream.try_next().await? {
}
Prefix with "Consistent" Precedence and Sugar
while let Some(value) = await? stream.try_next() {
}
Prefix with Mandatory Delimiters
while let Some(value) = await { stream.try_next() }? {
}

Would be the current way to do things (plus await).


I'll note that this example makes "prefix with delimiters" look especially bad to me. While await(...)? might look a touch nicer, if we were to do "prefix with delimiters" I'd hope we would only allow _one_ kind of delimiter ( like try { ... } ).

Quick tangent, but wouldn't "await with delimiters" just be... normal prefix await? await awaits an expression, so having await expr and await { expr } are essentially the same. It wouldn't make sense to only have await with delimiters and not have it without too, especially since any expression can be surrounded by {} and continue being the same expression.

The question about streams led me to the thought that for Rust it could be quite natural to have awaiting available not only as an operator in expressions, but also as a modifier in patterns:

// These two lines mean the same - both await the future
let x = await my_future;
let async x = my_future;

which then can naturally work with for

for async x in my_stream { ... }

@ivandardi

The problem that "prefix await with mandatory delimiters" solves is the precedence question around ?. With _mandatory_ delimiters, there is no precedence question.

See try { .... } for _similar_ (stable) syntax. Try has mandatory delimiters for much the same reason – how it interacts with ? – as a ? inside the braces is very different than one outside.

@yazaddaruvala I don't think there should be an async specific way to handle a for loop with results nicer, that should just come from some generic feature that also deals with Iterator<Item = Result<...>>.

// postfix syntax
for response in stream await {
    ...
}

This implies that stream: impl Future<Output = Iterator> and you're waiting for the iterator to be available, not each element. I don't see much chance for something better than async for item in stream (without a more general feature like @tanriol mentions).


@ivandardi the difference is precedence once you start chaining onto the end of the delimiters, here's two examples that parse differently with "obvious precedence prefix", "useful precedence prefix" (at least my understanding of what the "useful" precedence is) and "mandatory delimiters prefix".

await (foo.bar()).baz()?;
await { let foo = quux(); foo.bar() }.baz()?;

which parse to (with enough delimiters that these should be unambiguous for all three variants)

// obvious precedence prefix
await ((foo.bar()).baz()?);
await ({ let foo = quux(); foo.bar() }.baz()?);

// useful precedence prefix
(await ((foo.bar()).baz()))?;
(await ({ let foo = quux(); foo.bar() }.baz())?;

// mandatory delimiters prefix
(await (foo.bar())).baz()?;
(await { let foo = quux(); foo.bar() }).baz()?;

To my mind, @scottmcm's examples from C# in https://github.com/rust-lang/rust/issues/57640#issuecomment-457457727 look just fine with the delimiters moved from the (await foo).bar position to the await(foo).bar position:

await(response.Content.ReadAsStringAsync()).Should().Be(text);
var previous = await(branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Et cetera. This layout is familiar from normal function calls, familiar from existing keyword-based control flow, and familiar from other languages (including C#). This avoids breaking the strangeness budget, and doesn't seem to cause any problems for post-await chaining.

Though it admittedly has the same problem as try! for multi-await chaining, that doesn't seem to be as big of a deal as it was for Result? I would much rather limit the syntax strangeness here than accommodate a relatively uncommon and arguably unreadable pattern.

and familiar from other languages (including C#)

This is not how the precedence works in C# (or Javascript, or Python)

await(response.Content.ReadAsStringAsync()).Should().Be(text);

is equivalent to

var future = (response.Content.ReadAsStringAsync()).Should().Be(text);
await future;

(the dot operator has higher precedence than await, so binds more tightly even if you attempt to make await look like a function call).

I know, that's not the claim I was making. Merely that the ordering remains the same, so the only thing people need to add is parentheses, and that (separately) the existing function call syntax has the right precedence.

This layout is [...] familiar from existing keyword-based control flow

I disagree. We in fact _warn against_ using that layout in existing keyword-based control flow:

warning: unnecessary parentheses around `return` value
 --> src/lib.rs:2:9
  |
2 |   return(4);
  |         ^^^ help: remove these parentheses
  |
  = note: #[warn(unused_parens)] on by default

And rustfmt changes that to return (4);, _adding in_ a space.

That doesn't really affect the point I'm trying to make, and feels like splitting hairs. return (4), return(4), it's nothing more than a style issue.

I read the entire issue and I started feeling that {} braces and prefix are fine, but then it dissapeared

Let's see:

let foo = await some_future();
let bar = await {other_future()}?.bar

Looks nice, isn't it? But what if we want to chain more await's in one chain?

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

IMHO it looks much worse than

let foo = some_future() await;
let bar = other_future() await?
           .bar_async() await?;

However, being said, I don't believe in async chaining, as written in @cenwangumass 's post. Here is my example of using async/await in hyper server, please, show where chaining could be useful using this concrete example. I'm really tempted, no jokes.


About previous examples, most of them are "corrupted" in some way by combinators. You almost never need them since you get await. E.g.

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

is just

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())]) await?;
      Ok(res.json() await?)
   })
   .join_all() await
   .collect()?

if futures may return an error is just as simple as:

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())])? await?;
      Ok(res.json()? await?)
   })
   .join_all()? await
   .collect()?

@lnicola, @nicoburns,

I've created Pre-RFC thread for val.[await future] syntax on https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await-chaining/9304

@Dowwie procedural questions should be discussed somewhere else, for example https://internals.rust-lang.org

I think we have two camps in this discussion: people that would like to emphasize yield points vs. people that would like to de-emphasize them.

Async in Rust, circa 2018 contains the following blurb:

Async/await notation is a way of making asynchronous programming more closely resemble synchronous programming.

Taking the simple tokio example from above (changing unwrap() to ?):

let response = await!({
    client.get(uri).timeout(Duration::from_secs(10))
})?;

and applying postfix sigil leads to

let response = client.get(uri).timeout(Duration::from_secs(10))!?

which closely resembles synchronous programming and lacks distracting interspersed await of both prefix and postfix notation.

I used ! as postfix sigil in this example even though that suggestion got me some downvotes in a previous comment. I did so because the exclamation mark has an inherent meaning to me in this context which both @ (that my brain reads as "at") and # lack. But that's simply a matter of taste and not my point.

I'd simply prefer any single-character postfix sigil very much over all other alternatives precisely because it's very unobtrusive, thus making it easier to get a grip on what the code you are reading is actually doing versus whether or not it is asynchronous, which I'd consider an implementation detail. I wouldn't say it's not important at all but I'd argue that it's way less important for the flow of the code than the early return in case of ?.

To put it another way: you mainly care about yield points while writing asynchronous code, not so much while reading it. Since code is read more often than written, an unobtrusive syntax for await would be helpful.

I'd like to remind C# team experience (emphasizes are mine):

This is also why we didn't go with any 'implicit' form for 'await'. In practice it was something people wanted to think very clearly about, and which they wanted front-and-center in their code so they could pay attention to it. Interestingly enough, even years later, this tendency has remained. i.e. sometimes we regret many years later that something is excessively verbose. Some features are good in that way early on, but once people are comfortable with it, are better suited with something terser. That has not been the case with 'await'. People still seem to really like the heavy-weight nature of that keyword

Sigil characters are almost invisible without proper highlighting and are less friendly for newcomers (as I probably said before).


But talking about sigils, @ probably won't confuse non-english speakers (e.g. me) because we don't read it as at. We have a completely another name for it which doesn't trigger automatically when you read the code so you just understand it as a whole hieroglyph, with its own meaning.

@huxi I get the feeling that sigils are already pretty ruled out already. See Centril's post again for reasons why we should use the word await.


Also, for everyone that doesn't like postfix await that much, and use the pragmatic argument of "well, there isn't much real-life code out there that could be chained, so therefore postfix await shouldn't be added", here's a little anecdote (that I will probably butcher due to bad memory):

Back in World War II, one of the fighting nations were losing a lot of planes out there. They had to reinforce their planes somehow. So the obvious way to know where they should focus is look at the planes that came back and see where the bullets hit the most. Turns out that in a plane, an average of 70% of holes were in the wings, 10% in the engine area, and 20% in other areas of the plane. So with those statistics, it would make sense to reinforce the wings, right? Wrong! The reason for that is that you're only looking at the planes that came back. And in those planes, it seems like bullet damage on the wings isn't that bad. However, all the planes that came back only sustained minor damage to the engine area, which can lead to the conclusion that major damage to the engine area is fatal. So the engine area should be reinforced instead.

My point with this anecdote is: maybe there aren't a lot of real-life code examples that can take advantage of postfix await because there is no postfix await in other languages. So everyone is used to writing prefix await code and is very used to it, but we can never know if people would start chaining awaits more if we had postfix await.

So I think the best course of action would be to leverage the flexibility that nightly builds provide us and choose one prefix syntax and one postfix syntax to add to the language. I vote for the "Obvious Precedence" prefix await, and the .await postfix await. These aren't the final syntax choices, but we need to choose one to start with, and I think those two syntax choices would provide a newer experience compared with other syntax choices. After we have them implemented in nightly, then we can get usage statistics and opinions of working with real code using both options and we can then continue the syntax discussion, this time backed with a better pragmatic argument.

@ivandardi The anecdote is pretty heavyhanded and IMHO doesn't fit: it's a motivating tale at the beginning of a journey, to remind people to look for the non-obvious and cover all angles, it is _not_ one to be used against opposition in a discussion. It was fitting for Rust 2018, where it was raised and not bound to a specific issue. To use it against another side in a debate is impolite, you'd need to assert the position of the person that sees more or has more vision to make it work. I don't think this is what you want. Also, staying in the picture, maybe postfix is in none of the languages because postfix never made it home ;).

People have actually measured and looked: we do have a chainable operator in Rust (?) which is rarely used for chaining. https://github.com/rust-lang/rust/issues/57640#issuecomment-458022676

It has also been nicely covered that this is _only_ a measurement of the current state, as @cenwangumass has nicely put here: https://github.com/rust-lang/rust/issues/57640#issuecomment-457962730. So it's not like people are wielding this as final numbers.

I do want a story though where chaining becomes a halfway dominant style if postfix is really the way to go. I'm not convinced of that, though. I have also mentioned above that the dominant example wielded around here (reqwest) only requires 2 awaits because the API chose so and a convenient chain API without the need for 2 awaits can easily built today.

While I do appreciate the want for a research phase, I'd like to point out that await is already delayed gravely, any research phase will make that worse. We also have no way to collect statistics on many codebases, we'd have to a assemble a speaking set ourselves. I'd love more user research here, but that takes time, setup that doesn't exist yet and people to run it.

@Pzixel because of let rebinding, I think you could write

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

as

```
let foo = await some_future();
let bar = await { other_future() }?.bar_async();
let bar = await { bar }?;

@Pzixel Do you have any source for that C# team experience quote? Because the only one I could find was this comment. This isn't meant as an accusation or something. I'd just like to read the full text.

My brain translates @ to "at" because of its use in email addresses. That symbol is called "Klammeraffe" in my native language which roughly translates to "clinging monkey". I actually appreciate that my brain settled for "at" instead.

My 2 cents, as a relatively new Rust user (with a C++ background, but that doesn't really help).

Some of you mentioned newcomers, here is what I think:

  • first of all, an await!( ... ) macro seems indispensable to me, since it's dead simple to use and it cannot be misunderstood. Looks similar to println!(), panic!(),... and that's what happened for try! after all.
  • Mandatory delimiters are also simple and unambiguous.
  • a postfix version, either field, function or macro, wouldn't be difficult to read or write IMHO, since newcomers would just say "ok, that's how you do it". This argument holds for postfix keyword as well, that's "unusual" but "why not".
  • regarding prefix notation with useful precedence, I think that it would look confusing. The fact that await binds tighter will blow some minds and I think that some users will just prefer putting a lot of parenthesis to make it clear (chaining or not).
  • obvious precedence without sugar is simple to understand and to teach. Then, to introduce the sugar around it, just call await? a useful keyword. Useful because it remove the need for cumbersome parenthesis:
    ``c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids?await ...unwraps the future, so you have to use?to unwrap the Result // but there is some sugar if you want, thanks to theawait?` operator
    let response = await? http::get("https://www.rust-lang.org/");
    // but you shouldn't chain, because this syntax doesn't lead to readable chained code
- sigils can be understood quite easily *if the chosen character makes sense* if it is introduced to be "the `?` for futures".

That being said, since no agreement seems to be reached, I think it would be reasonable to ship `await!()` to stable Rust. Then this discussion can be extended without blocking the whole process. Same that what happened for `try!()`/`?`, so again newcomers won't be lost. And if [Simple postfix macros](https://github.com/rust-lang/rfcs/pull/2442) get accepted, the problem will disappear since we'll get postfix macro for "free".

---

Just a thought, what about a postfix keyword, but which can be put as prefix as well, similar in some ways to the `const` keyword of C++? (I don't know if that was already proposed) In prefix position, it behaves like "prefix `await` with obvious precedence and optional sugar":
```c#
// preferred without chaining:
let response = await? http::get("https://www.rust-lang.org/");

// but also possible: (rustfmt warning)
let response = http::get("https://www.rust-lang.org/") await?;
let response = (http::get("https://www.rust-lang.org/") await)?;
let response = (await http::get("https://www.rust-lang.org/"))?;

// chains well
let matches = http::get("https://www.rust-lang.org/") await?
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;

// any of these are also allowed, but arguably ugly (rustfmt warning again)
let matches = await ((http::get("https://www.rust-lang.org/") await?)
    .body?
    .async_regex_search("(?=(\d+))\w+\1"));
let matches = (await? http::get("https://www.rust-lang.org/"))
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;
let matches = await http::get("https://www.rust-lang.org/") await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1");
let matches = await (await http::get("https://www.rust-lang.org/"))?
    .body?
    .async_regex_search("(?=(\d+))\w+\1");
let matches = await!(
    http::get("https://www.rust-lang.org/")) await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
);
let matches = await { // <-- parenthesis or braces optional here, but they clarify
    (await? http::get("https://www.rust-lang.org/"))
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
};

How to teach that:

  • (await!() macro possible)
  • prefix recommended when no chaining happens, with sugar (see above)
  • postfix recommended with chaining
  • it's possible to mix them, but not recommended
  • possible to use prefix when chaining with combinators

By my own personal experience, I would say that the prefix await is not a problem for chaining.
Chaining appends a lot in Javascript code with prefix await and combinators like f.then(x => ...) without losing any readability in my opinion and they don't seem to feel any need to trade combinators for postfix await.

To do:

let ret = response.await!().json().await!().to_string();

is the same as:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

I don't really see the benefits of postfix await above combinator chains.
I find it easier to understand what happens in the second example.

I don't see any readability, chaining or precedence issues in the following code:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

I would be in favor of this prefix await because:

  • of the familiarity with other languages.
  • of the alignement with the other keywords (return, break, continue, yield, etc...).
  • it still feels like Rust (with combinators like we do with iterators).

Async/await will be a great addition to the language and I think more people are gonna use more futures-related stuff after this addition and its stabilization.
Some might even discover asynchronous programming for the first time with Rust.
So it might be beneficial to keep the language complexity down while the concepts behind async can be hard to learn already.

And I don't think shipping both prefix and postfix await is a good idea but this is just a personal opinion.

@huxi Yep, I already linked it above, but of course i can redo it: https://github.com/rust-lang/rust/issues/50547#issuecomment-388939886

My brain translates @ to "at" because of its use in email addresses. That symbol is called "Klammeraffe" in my native language which roughly translates to "clinging monkey". I actually appreciate that my brain settled for "at" instead.

I have similar story: in my language it's "dog", but it doesn't affect email reading.
Nice to see your experience though.

@llambda the question was about chaining. Of course you could just introduce extra variables. But anyway this await { foo }? instead of await? foo or foo await? looks werid.

@totorigolo
Nice post. But I don't think your second suggestion is a good way to go. When you introduce two ways to do something you only produce confusion and troubles, e.g. you need rustfmt option or your code becomes a mess.

@Hirevo async/await are supposed to remove need in combinators. Let's not return to "but you can do it with combinators alone". Your code is the same as

future.then(|x| x.json()).map(|x| x.to_string()).map(|ret| ... );

So let's remove async/await alltogether?

Being said, combinators are less expressive, less convinient, and sometimes you just can't express what await could, e.g. borrowing between await points (what Pin was designed for).

I don't see any readability, chaining or precedence issues in the following code

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

I do:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = serde_json::from_str(user);
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = serde_json::from_str(permissions );
    Ok(user)
}

Something weird is going on? No problem:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = dbg!(fetch(format!("/user/{0}", name).as_str()) await?);
    let user: User = dbg!(serde_json::from_str(user));
    let permissions = dbg!(fetch(format!("/permissions/{0}", x.id).as_str()) await?);
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions));
    Ok(user)
}

It's more cumbersome to get it work with combinators. Not to mention that your ? in then/map functions won't work as expected, and your code won't work without into_future() and some other weird things you don't need in async/await flow.

I agree that my proposition isn't optimal in that it introduces a lot of legal syntax using the async keyword. But I believe that the rules are easy to understand and that they satisfy all our use cases.

But again, that would mean a lot of allowed variations:

  • await!(...), for consistency with try!() and well-known macros like println!()
  • await, await(...), await { ... }, ie. prefix syntax without sugar
  • await?, await?(), await? {}, ie. prefix syntax with sugar
  • ... await, ie. postfix syntax (plus ... await?, but which isn't sugar)
  • all combinations of those, but which aren't encouraged (see my previous post)

But in practice we only expect to see:

  • prefix without Results: await, or await { ... } to clarify with long expressions
  • prefix with Results: await?, or await? {} to clarify with long expressions
  • postfix when chaining: ... await, ... await?

+1 to ship await!() macro to stable. Tomorrow, if possible 😄

There's much speculation on how this pattern _will_ be used and concerns about ergonomics in such cases. I appreciate these concerns, but I don't see a compelling reason why this couldn't be an iterative change. This will allow for the generation of real usage metrics that can later inform optimization (assuming it's needed).

If the adoption to async Rust is a larger effort for major crates/projects than any later _optional_ refactoring effort to move to a syntax more concise/expressive, then I strongly advocate we allow that adoption effort to begin right now. The continued uncertainty is causing pain.

@Pzixel
(First, I made an error calling the function fetch_user, I'll fix it in this post)

I'm not saying that async/await is useless and that we should remove it for combinators.
Await allows to bind a value out of a future to a variable within the same scope after it is resolved which is really useful and not even possible with combinators alone.
Removing await was just not my point.
I just said that combinators can play very nicely with await, and that the chaining problem can be addressed by only awaiting the whole expression built by combinators (removing the need for await (await fetch("test")).json() or await { await { fetch("test") }.json() }).

In my code example, the ? does behave as intended by short-circuiting, making the closure return an Err(...), not the whole function (that part is handled by the await? on the whole chain).

You effectively rewritten my code example but you removed the chaining part of it (by doing bindings).
For instance, for the debugging part, the following has the exact same behavior with just the necessary dbg! and nothing more (not even extra parentheses):

async fn fetch_permissions(name: &str) -> Result<Vec<Permission>, Error> {
    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| dbg!(serde_json::from_str::<User>(dbg!(x)?)))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str())))
        .map(|x| dbg!(serde_json::from_str::<Vec<Permission>>(dbg!(x)?)));
    Ok(user)
}

I don't know how to do the same without combinators and no additional bindings or parentheses.
Some people do chaining to avoid filling the scope with temporary variables.
So I just wanted to say that combinators can be useful and should not be ignored when making a decision about a particular syntax regarding its chaining ability.

And, lastly, out of curiosity, why the code wouldn't work without .into_future(), aren't they already futures (I am not an expert on this, but I would expect them to be already futures) ?

I would like to highlight a significant problem with fut await: it seriously messes with how people read code. There is a certain value in programming expressions being similar to phrases used in a natural language, this is one of the reasons why we have constructs like for value in collection {..}, why in most of the languages we write a + b ("a plus b") instead of a b +, and writing/reading "await something" is much more natural for English (and other SVO languages) than "something await". Just imagine that instead of ? we would've used a postfix try keyword: let val = foo() try;.

fut.await!() and fut.await() don't have this problem because they look like familiar blocking calls (but "macro" additionally emphasizes the associated "magic"), so they will be perceived differently from a space separated keyword. Sigil also will be perceived differently, in a more abstract way, without any direct parallels with natural language phrases, and for this reason I think it's irrelevant how people read proposed sigils.

In conclusion: if it will be decided to stick with the keyword, I strongly believe we should choose from prefix variants only.

@rpjohnst I'm not sure what your point is, then. actual_fun(a + b)? and break (a + b)? matter to me because of the different precedence, so I don't know what await(a + b)? is supposed to be.

I've been following this discussion from afar and I have a few questions and comments.

My first comment is that I believe the .await!() postfix macro satisfies all of Centril's main objectives with the exception of the first point:

"await should remain a keyword to enable future language design."

What other uses of the await keyword do we see in the future?


Edit: I misunderstood how exactly await functions. I've struck through my incorrect statements and updated the examples.

My second comment is that await keyword in Rust does completely different things from await in other language and this has the potential to cause confusion and unexpected race conditions. For example consider this JavaScript snippet which executes each Promise in series:

async function waitFor6SecondThenReturn6(){
  let result1 = await waitFor1SecondThenReturn1(); // executes first
  let result2 = await waitFor2SecondThenReturn2(); // executes second
  let result3 = await waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

I believe that a prefix async does a much clearer job indicating that the async values are pieces of a compiler-constructed state machine:

async function waitFor6SecondThenReturn6(){
  let async result1 = waitFor1SecondThenReturn1(); // executes first
  let async result2 = waitFor2SecondThenReturn2(); // executes second
  let async result3 = waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

It's intuitive that async functions allow you to use async values. This also builds intuition that there's asynchronous machinery at work in the background and how it might be working.

This syntax has some unresolved questions and clear composability issues, but it makes clear where asynchronous work is happening and it serves as a synchronous-styled complement to the more composable and chainable .await!().

I had difficulty noticing the postfix await at the end of some expressions in earlier procedural-style examples so here's what that would look like with this proposed syntax:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let async user = dbg!(fetch(format!("/user/{0}", name).as_str()));
    let user: User = dbg!(serde_json::from_str(user?));
    let async permissions = dbg!(fetch(format!("/permissions/{0}", user.id).as_str()));
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions?));
    Ok(user)
}

(There's also the argument to be made for an easily composable and chainable .dbg!() macro, but that's for a different forum.)

On Tue, Jan 29, 2019 at 11:31:32PM -0800, Sphericon wrote:

I've been following this discussion from afar and I have a few questions and comments.

My first comment is that I believe the .await!() postfix macro satisfies all of Centril's main objectives with the exception of the first point:

"await should remain a keyword to enable future language design."

As one of the primary proponents of the .await!() syntax, I
absolutely think await should remain a keyword. Given that we'll
need .await!() to be built into the compiler anyway, that seems
trivial.

@Hirevo

You effectively rewritten my code example but you removed the chaining part of it (by doing bindings).

I don't get this "chaining for chaining" approach. Chaining is good when it's good, e.g. it doesn't make to create useless temp variables. You say "you removed chaining", but you can see that it provides no value here.

For instance, for the debugging part, the following has the exact same behavior with just the necessary dbg! and nothing more (not even extra parentheses)

No, you did this

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = dbg!(serde_json::from_str(dbg!(user)));
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(dbg!(permissions));
    Ok(user)
}

It's not the same thing.

I just said that combinators can play very nicely with await, and that the chaining problem can be addressed by only awaiting the whole expression built by combinators (removing the need for await (await fetch("test")).json() or await { await { fetch("test") }.json() }).

It's only a problem for prefix await, it doesn't exist for other forms of it.

I don't know how to do the same without combinators and no additional bindings or parentheses.

Why not create these bindings? Bindings are always better for reader, for instance, and if you have a debugger which could print a binding value, but which couldn't evaluate an expression.

Finally, you didn't actually removed any bindings. You just used labmda |user| syntax where I did let user = .... It didn't save you anything, but now this code is harder to read, harder to debug, and it doesn't have access to the parent (e.g. you have to wrap errors externally in method chain instead of doing it it call itself, probably).


In a nutshell: chaining doesn't provide value by itself. It may be useful in some scenarios, but it's not one of them. And since I write async/await code for more than six years, I do believe you don't want write chained async code ever. Not because you couldn't, but because it's inconvenient and binding-approach is always nicer to read and often to write. Not to say that you don't need combinators at all. If you have async/await, you don't need these hundreds of methods on futures/streams, you only need two: join and select. Everything else may be done via iterators/bindings/..., i.e. common language tools, that don't make you to learn yet another infrastructure.

@Sphericon AFAIK community agreed on using await as either keyword or a sigil, so imho your ideas about async require another RFC.

My second comment is that await keyword in Rust does completely different things from await in other language and this has the potential to cause confusion and unexpected race conditions.

Can you be more detailed? I didn't see any difference between JS/C# await's, except futures being poll-based, but it have really nothing to deal with async/await.

Regarding the await? prefix syntax proposed in several places here:
```C#
let foo = await? bar_async();

How would this look with ~~futures of futures~~ result of results *) ? I.e., would it be arbitrarily extensible:
```C#
let foo = await?? double_trouble();

IOW, prefix await? looks like a syntax that is too special-cased to me.

) *edited. Thanks to @Nemo157 for pointing it out.

How would this look with futures of futures? I.e., would it be arbitrarily extensible:

let foo = await?? double_trouble();

IOW, await? looks like a syntax that is too special-cased to me.

@rolandsteiner by "futures of futures" do you mean impl Future<Output = Result<Result<_, _>, _>> (one await + two ? implies "unwrapping" a single future and two results to me, not awaiting nested futures).

await? _is_ a special-case, but it's a special-case that will likely apply to 90%+ uses of await. The entire point of futures is as a way to wait on asynchronous _IO_ operations, IO is fallible so 90%+ of async fn will likely return io::Result<_> (or some other error type that includes an IO variant). Functions that return Result<Result<_, _>, _> are pretty rare currently, so I wouldn't expect them to require special-case syntax.

@Nemo157 You're right of course: Result of Results. Updated my comment.

Today, we write

  1. await!(future?) for future: Result<Future<Output=T>,E>
  2. await!(future)? for future: Future<Output=Result<T,E>>

And if we write await future? we have to work out which one it means.

But is it the case that case 1 can always turned into case 2? In case 1, the expression either produce a future, or an error. But the error can be delayed and moved inside the future. So we can just handle case 2 and make an automatic convertsion happening here.

In the programmer's point of view, Result<Future<Output=T>,E> garantees early return for the error case, but except that the two have the same sementic. I can image the compiler can work out and avoid the additional poll call if the error case is immidiate.

So the proposal is:

await exp? can be interpreted as await (exp?) if exp is Result<Future<Output=T>,E>, and interpreted as (await exp)? if exp is Future<Output=Result<T,E>>. In both cases, it will early return in error, and resolve to the true result if running ok.

For more complicated cases, we can apply something like the automatic method receiver dereference:

> When interperting await exp???? we first check exp and if is Result, try it and keep going when the result is still a Result until running out of ? or have something that is not Result. Then it have to be a future and we await on it and apply the rest ?s.

I was a postfix keyword/sigil supporter and still am. However I just want to show that the prefix precedence may not be a big issue in pratice and have workarounds.

I know Rust team members don't like implicit things, but in such a case, it is just have too little difference between the potential sementics and we have a good way to ensure we do the right thing.

await? is a special-case, but it's a special-case that will likely apply to 90%+ uses of await. The entire point of futures is as a way to wait on asynchronous IO operations, IO is fallible so 90%+ of async fn will likely return io::Result<_> (or some other error type that includes an IO variant). Functions that return Result<Result<_, _>, _> are pretty rare currently, so I wouldn't expect them to require special-case syntax.

Special cases are bad to compose, expand, or learn, and eventually turn into baggage. It's not a good compromise to make exceptions to the rules of the language for a single theoretical usability use-case.

Would it be possible to implement Future for Result<T, E> where T: Future? That way you could just await result_of_future without needing to unwrap it with ?. And that of course would return a Result, so you'd call it await result_of_future, which would mean (await result_of_future)?. That way we wouldn't need the await? syntax and the prefix syntax would be a bit more consistent. Let me know if there's anything wrong with this.

Additional arguments for await with mandatory delimiters include (personally I'm not sure which syntax I like best overall):

  • No special casing of the ? operator, no await? or await??
  • Congruent with existing control-flow operators such as loop, while, and for, which also require mandatory delimiters
  • Feels most at home to similar existing Rust constructs
  • Eliminating special casing helps avoid trouble when writing macros
  • Doesn't use sigils or postfix, avoiding spending from the strangeness budget

Example:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

However, after playing around with this in an editor, it feels cumbersome still. I think I would rather have await and await? without delimiters, like with break and return.

Would it be possible to implement Future for Result where T: Future?

You would want the inverse. The most common awaitable is a Future where it's output type is a Result.

There is then the explicit argument agaisnt hiding or otherwise absorbing the ? into just await. What if you want to match on the result, etc.

If you have a Result<Future<Result<T, E2>>, E1>, awaiting it would return a Result<Result<T, E2>, E1>.

If you have a Future<Result<T, E1>>, then awaiting it would simply return the Result<T, E1>.

There's no hiding or absorbing the ? into the await, and you can do whatever is needed with the Result afterwards.

Oh. I must have misunderstood you then. I don't see how that helps though as we still need to combine the ? with await 99% of the time.


Oh. The await? syntax is supposed to imply (await future)? which would be the common case.

Exactly. So we'd just make the await bind tighter in await expr?, and if that expression is a Result<Future<Result<T, E2>>, E1> then it would evaluate to something of the type Result<T, E2>. It would mean there is no special casing for awaiting on Result types. It just follows the normal trait implementations.

@ivandardi what's about Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@Pzixel Note, that form of future is gone. There is a single associated type now, Output (which probably will be a Result).


@ivandardi Okay. I see it now. The only thing you'd have against you there is the precedence being something strange you'd have to learn there as it's a deviation but so is most of anything with await I suppose.

Though a Result that returns a future is so rare I haven't found one case apart from something in tokio core that was removed so I don't think we need any sugar/trait impls to help in that case.

@ivandardi what's about Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

Well, I'd assume that that is not possible, seeing that the Future trait only has a Output associated type.


@mehcode Well, it does address some of the concerns that were brought up previously, I'd say. It also helps decide on the prefix syntax, because there would be just one prefix await syntax instead of "Obvious Precedence" and "Useful Precedence" choices.

Well, I'd assume that that is not possible, seeing that the Future trait only has a Output associated type.

why not?

fn probably_get_future(val: u32) -> Result<impl Future<Item=i32, Error=u32>, &'static str> {
    match val {
        0 => Ok(ok(15)),
        1 => Ok(err(100500)),
        _ => Err("Coulnd't create a future"),
    }
}

@Pzixel See https://doc.rust-lang.org/std/future/trait.Future.html

You are talking about the old trait that was in the futures crate.

Honestly, I don't think that having a keyword in prefix position how it was supposed to be:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (yield self.request(url, Method::GET, None, true)))?;
    let user = (yield user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}

has any strong advantage over having a sigil in the same position:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (*self.request(url, Method::GET, None, true))?;
    let user = (*user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}

I think that it only looks inconsistently with other prefix operators, adds redundant whitespace before expression, and shifts code to the right side at noticeable distance.


We can try using sigil with extended dot syntax (Pre-RFC) which resolves issues with deeply nested scopes:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[*request(url, Method::GET, None, true)]?;
    let user = user.res.[*json::<UserResponse>()]?;
    let user = user.user.into();
    Ok(user)
}

as well as adds possibility to chain methods:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[*request(url, Method::GET, None, true)]?
        .res.[*json::<UserResponse>()]?
        .user
        .into();
    Ok(user)
}

And obviously, let substitute * with @ which makes more sense here:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (@self.request(url, Method::GET, None, true))?;
    let user = (@user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[@request(url, Method::GET, None, true)]?;
    let user = user.res.[@json::<UserResponse>()]?;
    let user = user.user.into();
    Ok(user)
}
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[@request(url, Method::GET, None, true)]?
        .res.[@json::<UserResponse>()]?
        .user
        .into();
    Ok(user)
}

What I like here is that @ reflects to await which is placed on LHS of function declaration, while ? reflects to Result<User> which is placed on the RHS of function declaration. This makes @ extremely consistent with ?.


Any thoughts on this?

Any thoughts on this?

Any extra braces are just a no-go

@mehcode Yep, I didn't realize that futures now lack Error type. But the point is still valid: you can have a funtion, that probably return a feature, which, when completed, may return result or error.

Any thoughts on this?

Yes! According to Centril's comment, sigils aren't very greppable. I'd only start to consider sigils if one can create a regex that identifies all the await points.

As for your extended dot syntax proposal, you'd have to explain it a lot deeper, providing the semantics and desugaring of every usage of it. As of currently, I can't understand the meaning of the code snippets you posted with that syntax.


But the point is still valid: you can have a funtion, that probably return a feature, which, when completed, may return result or error.

So you'd have a Future<Item=Result<T, E>>, right? Well, in that case, you just... await the future and deal with the result 😐

As in, suppose that foo is of type Future<Item=Result<T, E>>. Then await foo will be a Result<T, E> and you can use the following to deal with the errors:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())

@melodicstream

So you'd have a Future>, right? Well, in that case, you just... await the future and deal with the result 😐

No, I mean Result<Future<Item=Result<T, E>>, OtherE>

With postfix variant you just do

let bar = foo()? await?.bar();

@melodicstream I updated my post and added link to pre-RFC for that feature

Oh dear... I just re-read all the comments for the third time...

The only consistent feeling is my dislike for the .await postfix notation in all its variations because I'd seriously expect the await to be part of Future with that syntax. The "power of the dot" may work for an IDE but doesn't work for me at all. I could certainly adapt to it if it is the stabilized syntax but I doubt that it will ever really feel "right" to me.

I'd go with the super basic await prefix notation without any mandatory brackets, mainly because of the KISS principle and because doing it similar to most other languages is worth a lot.

De-sugaring await? future to (await future)? would be fine and appreciated but anything beyond that seems more and more like a solution without a problem to me. Simple let rebinding improved the readability of the code in most examples and I, personally, would likely go down that route while writing code even if easy chaining were an option.

With that being said, I'm quite happy that I'm not in a position to decide about this.

I'll now leave this issue alone instead of adding further noise and await (pun intended) the final verdict.

... at least I'll honestly try to do that...

@Pzixel Assuming foo is of type Result<Future<Item=Result<T, E1>>, E2>, then await foo would be of type Result<Result<T, E1>, E2> and then you can just deal with that Result accordingly.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();

@melodicstream no, it won't. You cannot await Result, you can awaitFuture. So you have to dofoo()?to unwrapFuturefromResult, then doawaitto get a result, then again?` to unwrap result from the future.

In postfix way it will be foo? await?, in prefix... I'm not sure.

So your examples just don't work, especially the last one, because it should be

(await foo.unwrap()).unwrap()

However, @huxi may be right we are solving the problem that probably doesn't exist. The best way to figure it out it allow postfix macro and see real codebase after basic async/await adoption.

@Pzixel That's why I made the proposal to implement Future on all types of kind Result<Future<Item=T>, E>. Doing that would allow what I'm saying.

Although I am OK with await foo?? for Result<Future<Output=Result<T, E1>>, E2>, I am NOT happy with await foo.unwrap().unwrap(). In my first brain model this have to be

(await foo.unwrap()).unwrap()

Otherwise I will be really confusing. The reason is that ? is a general operator, and unwrap is a method. The compiler can do something special for operators like the ., but if it is a normal method, I will assume it is always relates to the closest expression in its left hand side, only.

The postfix syntax, foo.unwrap() await.unwrap(), is also OK to me, as I know that await is just a keyword, not an object, so it have to be part of the expression before unwrap().

postfix style macro solves much of these problems nicely, but just the question of do we want to retain familiarity with existing languages and keep it prefixed. I'd vote for postfix style.

Am I right that following code is not equal:

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      do_async_call().map(|_| 10)
   }
}

and

async fn foo(n: u32) -> u32 {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      await!(do_async_call());
      10
   }
}

The first one fill panic when trying to create a future, while the second one will panic on first poll. If so, should we do something with it? It could be workarounded like

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      async {
         await!(do_async_call());
         10 
      }
   }
}

But it's a lot less convenient.

@Pzixel: this is a decision that has been made. async fn are fully lazy and don't run at all until polled, and thus capture all input lifetimes for the entire future lifetime. The workaround is a wrapping function returning impl Future being explicit, and using an async block (or fn) for the deferred calculation. This is _required_ for stacking Future combinators without allocation in-between each one, as after the first poll they can't be moved.

This is a done decision and why implicit await systems don't work (well) for Rust.

C# developer here, and I'm going to advocate for the prefix style, so get ready to downvote.

Postfix syntax with a dot (read().await or read().await()) is very misleading and suggests a field access or method invocation, and await is not either of those things; it's a language feature. I'm not a seasoned Rustacean by any means, but I'm not aware of any other .something cases that are effectively a keyword that will be rewritten by the compiler. Closest I can think of is the ? suffix.

Having had async/await in C# for a few years now, the standard prefix-with-a-space syntax (await foo()) has not caused me any problems in all that time. In cases where a nested await is required it's not onerous to use parens, which is standard syntax for all explicit operator precedence and therefore easy to grok, e.g.

```c#
var list = (await GetListAsync()).ToList();


`await` is essentially a unary operator, so treating it as such makes sense.

In C# 8.0, currently in preview, we have async enumerables (iterators), which comes with an `IAsyncEnumerable<T>` interface and `await foreach (var x in QueryAsync())` syntax. The lack of async enumerables has been an issue since async was added in C# 5; for example, we have ORMs that return a `Task<IEnumerable>` which is only half asynchronous; we have to block on calls to `MoveNext()`. Having proper async enumerables is a big deal.

If a postfix `await` is used and similar async iterator support is added to Rust, that would look something like

```rust
for val in v_async_iter {
    println!("Got: {}", val);
} await // or .await or .await()

That seems weird.

I think consistency with other languages is also something to be given weight. At present an await keyword before the asynchronous expression is the syntax for C#, VB.NET, JavaScript and Python, and it's the proposed syntax for C++ as well. That's five out of the top seven languages in the world; C and Java don't have async/await yet, but I'd be surprised if they went another way.

Unless there are really good reasons to go a different way, or to put it another way, if both prefix and postfix weigh out the same in terms of advantages and compromises, I'd suggest that there's a lot to be said for the unofficially "standard" syntax.

@markrendle I'm C# dev too, writing async/await since 2013. And I like prefix too. But Rust differs a lot.

You probably know that your (await Foo()).ToList() is a rare case. In most cases you just write await Foo(), and if you need another type you probably want to fix the Foo signature.

But rust is another thing. We have ? here and we have to propagate errors. In C# async/await automatically wraps exceptions, throws it across await points and so on. You don't have it in rust. Consider every time you write await in C# you have to write

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

It's very tedious to have all these braces.

OTOH with postfix you have

var response = client.GetAsync("www.google.com") await.HandleException();
var json =  response.ReadAsStreamAsync() await.HandleException();
var somethingElse = DoMoreAsyncStuff(json) await.HandleException();
...

or in rust terms

let response = client.get_async("www.google.com") await?;
let json =  response.read_as_Stream_async() await?;
let somethingElse = do_more_async_stuff(json) await?;
...

And imaine we will have some another transformer, additionally to ? and await, let's name it @. If we invent extra syntax for await?, then we'l have to have await@, @?, await@?, ... to make it consistent.


I love prefix await too, but I'm not quite sure if Rust is fine with it. Having extra rules added into the language may be ok, but it's a burden that probably doesn't worth easier readability.

Re-read all comments including tracking issue thread (spent nearly 3 hours on this). And my summary is: the postfix @ looks like the best solution, and unfortunately it's the most underappreciated one.

There's a lot of (biased) opinions about it, and below are my comments for them:


async-await would be unfamiliar with postfix @

Actually, it would be only half unfamiliar because the same async fn syntax would be provided, and the same functionality as with await would be still available.

Rust has a lot of sigils and we shouldn't introduce a yet another sigil

But sigils are fine until they're readable and consistent with other syntax. And introducing a new sigil not strictly means that it will make things somehow worse. In consistency perspective postfix @ is a way more better syntax than any prefix/postfix await.

The snippets like ?!@# reminds me of Perl

In reality we would rarely see any other combination than @? which IMO looks pretty straightforward and ergonomic. Return types like impl Try<impl Future<T>>, impl Future<impl Try<impl Try<T>>>, and even impl Future<T>, would occur at most in 10% of usages as we could see on a real world examples ITT. Moreover, sigil conglomeration in 90% of these 10% would indicate a code smell when you either must fix your API or introduce a temporary binding instead to clarify it.

Postfix @ is so hard to notice!

And that's actually a big advantage in async-await context. This syntax introduces ability to express asynchronous code like it's synchronous, so more intrusive await only interferes with its original purpose. Examples above clearly displays that occurrence of yield points might be dense enough to bloat code when they are too explicit. Of course we must have them on sight, however @ sigil would be perfectly fine for that as well as await without bloating the code.

Awaiting future is costly operation and we should explicitly say that

It really make sense, however I don't think that we should say it so often and so loud. I think that there could be provided analogy with mutation syntax: mut is required explicitly only once and further we can use mutable binding implicitly. Another analogy could be provided with unsafe syntax: explicit unsafe is required only once and further we can dereference raw pointers, call unsafe functions, etc. And yet another analogy could be provided with bubble operator: explicit impl Try return or upcoming try block are required only once and further we can use ? operator confidently. So, in the same way explicit async annotates possibly durable operations only once, and further we can apply @ operator which actually would do it.

It's weird because I read it differently than await, and I can't type it easily

If you read & as "ref" and ! as "not", then I don't think that how you read @ currently is a strong argument to not use this symbol. It also don't matter how fast you type it because reading of code is always in more important priority. In both cases adopting to @ is very simple.

It's ambiguous because @ is already used in pattern matching

I don't think that's a problem because ! already is used in macros, & already is used in borrowing and in && operator, | is used in closures, in patterns, and in || operator, +/- could be applied in prefix position as well, and . could be used in yet more different contexts. There's nothing surprising and nothing wrong in reusing the same symbols in different syntax constructs.

It would be hard to grep

If you want to check if some sources contains asynchronous patterns then rg async would be much simpler and straightforward solution. If you really want to find all yield points in this way, then you would be able to write something like rg "@\s*[;.?|%^&*-+)}\]]" which isn't straightforward, but it's also not something mindblowing. Considering how often it will be required, IMO this regex is absolutely fine.

@ isn't beginner friendly, and it's hard to google

I think that it's actually easier to understand for beginners because it works consistently with ? operator. Contrarily, prefix/postfix await wouldn't be consistent with other Rust syntax additionally introducing headache with associativity/formatting/meaning. I don't think either that await would add some self-documenting value because single keyword cannot describe the whole underlying concept and beginners would learn it from the book anyway. And Rust never been a language which gives you a plain text hints for each of possible syntax constructs. If someone forgets what @ symbol means (I really doubt that it would be a problem because there's also many associations to remember it properly) - then googling for it should be successful as well because currently rust @ symbol returns all relevant information about @ in pattern matching.

We already have reserved await keyword, so we should use it anyway

Why? It's okay to have unused reserved keyword when it turns out that the final feature implementation is better without it. Also, no promise was given that exactly await keyword would be introduced. And this identifier is not very common in real world code, so we would lose nothing when it would remain reserved until the next edition.

? was a mistake and @ would be a second mistake

Probably you thinks more lexically, and because of that you prefer to work with words instead of symbols. And because of that you don't see any value in thinner syntax. But keep in mind that more people think more visually, and for them intrusive words are harder to work instead. Probably a good compromise here would be providing await!() macro alongside, like try!() was provided alongside with ?, therefore you would be able to use it instead of @ if you are really uncomfortable with @.

^^^^^^^ did you reread my comment too?

I had a large revelation reading @I60R's comment that has made me in favor of the postfix sigil, and I've not liked using a sigil until now (instead preferring some form of postfix await).

Our await operation isn't await. At least not in the way that await is used in C# and JavaScript, the two largest sources of async/await familiarity.

In these languages, the semantic of starting a Task (C#)/Promise (JS) is that the task is immediately put onto the task queue to be executed. In C# this is a multi-threaded context, in JS it's single-threaded.

But in either of these languages, await foo semantically means "park this task and await the completion of the task foo, and in the meantime the computational resources being used by this task can be used by other tasks".

This is why you get parallelism with separate awaits after constructing multiple Task/Promises (even on single thread executors): the semantics of await aren't "run this task" but rather "run some task" until finished awaiting and we can resume.

This is entirely separate from lazy- or eager- till-first-await starting of tasks when created, though heavily related. I'm taking about queueing the rest of the task after the synchronous work has been done.

We already have confusion along this line in users from lazy iterators and current futures. I posit await syntax isn't doing us any favors here.

Our await!(foo) thus isn't await foo in the "traditional" C# or JS way. So what language more closely related to our semantics? I now firmly believe that it is (@matklad correct me if I've got details wrong) Kotlin's suspend fun. Or as it's been referred to in Rust-adjacent discussions, "explicit async, implicit await".

Brief review: in Kotlin, you can only call a suspend fun from within a suspend context. This immediately runs the called function through to completion, suspending the stacked context as required by the child function. If you want to run child suspend fun in parallel, you create the equivalent of async closures, and combine those with a suspend fun combinator to run multiple suspend closures in parallel. (It's slightly more involved than that.) If you want to run a child suspend fun in the background, you create a task type which puts that task on the executor and exposes an .await() suspend fun method to join your task back up with the background task.

This architecture, though described with different verbs than Rust's Future, sounds very familiar.

I've previously explained why implicit await doesn't work for Rust, and stand by that position. We need async fn() -> T and fn() -> Future<T> to be equivalent in usage so that the "do some initial work" pattern is possible (to make lifetimes work in tricky cases) with lazy futures. We need lazy futures so that we can stack futures without a layer of indirection between each one for pinning.

But I'm now convinced the "explicit but quiet" syntax of foo@ (or some other sigil) makes sense. As much as I'm loathe to say the word here, @ in this case is a monadic operation in async much like ? is the monadic operation in try.

There's some opinion that sprinkling ? in your code is just to get the compiler to stop complaining when you're returning a Result. And in a sense, you are doing this. But it's to preserve our ideal of local clarity of code. I need to know whether this operation is unpacked through the monad or taken literally locally.

(If I got monad details wrong I'm sure someone will correct me, but I believe my point still stands.)

If @ behaves the same way, I posit this is a good thing. This isn't about spinning up some number of tasks then awaiting their finish, it's about building up a state machine that someone else needs to pump to completion, whether that be by building it into their own state machine or putting it into an execution context.

TL;DR: if you take one thing from this mini blog post, let it be this: Rust's await!() isn't await as it's in other languages. The results are _similar_ but the semantics of how tasks are handled are vastly different. From this we benefit from not using the common syntax, as we lack the common behavior.

(It was mentioned in a previous discussion a language that changed from lazy to eager futures. I believe this is because await foo suggests that foo is already happening, and we're merely awaiting it producing a result.)

EDIT: if you want to discuss this in a more sync fashion, ping me @​CAD on the rust development Discord (link is 24h) or internals or users (@​CAD97). I'd love to put my position up against some direct scrutiny.

EDIT 2: Sorry GitHub @​cad for the accidental ping; I tried to escape the @ and didn't mean to pull you into this.

@I60R

The snippets like ?!@# reminds me of Perl

In reality we would rarely see any other combination than @? which IMO looks pretty straightforward and

You can't know it until it appears. If we have yet another code transformer - yield sigil, for example - it would be a real Perl script.

Postfix @ is so hard to notice!

And that's actually a big advantage in async-await context.

Difficulty in noticing await points cannot be an advantage.

Awaiting future is costly operation and we should explicitly say that

It really make sense, however I don't think that we should say it so often and so loud.

Reread my links about C# experience, please. It's actually this important to say it this loud and this often.


Other points are quite valid, but those I've replyed to are unfixable gaps.

@CAD97 As a C# developer who can say how async is different in Rust.. Well, it's not that different as you describes. IMHO your implications are based on a false premise

Our await!(foo) thus isn't await foo in the "traditional" C# or JS way.

It's quite the same thing in C# dev mind. Of course, you should remember that futures won't be executed until polled, but in most cases it doesn't matter. In most cases await just means "I want to get unwrapped value from future F into variable x". Having future not running until polled is actually a relief after C# world so people will thankfully happy. Beside that, C# has similar concepts with Iterators/generatos, that are lazy too, so C# crowd is quite familiar with the whole thing.

In a nutshell, catching await working a bit different is easier than having a sigil working just like await in their %younameit% language, but not quite. People won't be confused by the difference since it's not that much the deal as you thing they might think. Situations when eager/lazy execution matters are really rare thus it shouldn't be the primary design concern.

@Pzixel It's quite the same thing in C# dev mind. Of course, you should remember that futures won't be executed until polled, but in most cases it doesn't matter.

Situations when eager/lazy execution matters are _really_ rare thus it shouldn't be the primary design concern.

Unfortunately that isn't true: the pattern of "spawn some Promises and then later await them" is very common and desirable, but it does not work with Rust.

That's a very big difference, which even caught some Rust members by surprise!

async/await in Rust really is closer to a monadic system, it has basically nothing in common with JavaScript/C#:

  • The implementation is completely different (building a state machine and then executing it on a Task, which is consistent with monads).

  • The API is completely different (pull vs push).

  • The default behavior of being lazy is completely different (which is consistent with monads).

  • There can only be one consumer, rather than many (which is consistent with monads).

  • Separating errors and using ? to handle them is completely different (which is consistent with monad transformers).

  • The memory model is completely different, which has a huge impact on both the implementation of Rust Futures, and also how users actually use Futures.

The only thing in common is that all the systems are designed to make asynchronous code easier. That's it. That's really not much in common at all.

Many many people have mentioned C#. We know.

We know what C# did, we know what JavaScript did. We know why those languages made those decisions.

Official team members of C# have talked to us and explained in detail why they made those decisions with async/await.

Nobody is ignoring those languages, or those data points, or those use cases. They are being taken into account.

But Rust really is different from C# or JavaScript (or any other language). So we cannot just blindly copy what other languages do.

But in either of these languages, await foo semantically means "park this task and await the completion of the task foo

It’s exactly the same in Rust. The semantics of async functions are the same. If you call an async fn (which creates a Future), it doesn’t yet start execution. That is indeed different to JS and C# world.

Awaiting it will drive the future to completion, which is the same as everywhere else.

Unfortunately that isn't true: the pattern of "spawn some Promises and then later await them" is very common and desirable, but it does not work with Rust.

Could you be in more details? I've read the post but didn't find what's wrong with

let foos = (1..10).map(|x| some_future(x)); // create futures, unlike C#/JS don't actually run anything
let results = await foos.join(); // awaits them

Of course, they won't fire until line 2, but in most cases it's a desirable behavior, and I'm still convinced this is a huge difference that don't allow to use await keyword anymore. The topic name itself hints that await word makes sense here.

Nobody is ignoring those languages, or those data points, or those use cases. They are being taken into account.

I won't be annoying repeating the same arguments over and over again. I think I've delivered what I supposed to, so I won't do it anymore.


P.S.

  • The API is completely different (pull vs push).

It makes difference when implementing your own futures only. But in 99(100?)% cases you just use combinators pf futures that hides this difference.

  • The memory model is completely different, which has a huge impact on both the implementation of Rust Futures, and also how users actually use Futures.

Could you be in more detail? I actually have played with async code already when writing a simple web server on Hyper and I didn't notice any difference. Put async here, await there, done.

The argument that rust's async/await has a different implementation from other languages and then we must do something different is pretty unconvincing. C's

for (int i = 0; i < n; i++)

and Python's

for i in range(n)

definitely don't share the same underlying mechanism but why does Python choose to use for? It should use i in range(n) @ or i in range(n) --->> or whatever to show this important difference!

The question here is whether the difference is important for the user's daily job!

A typical rust user's daily life is about:

  1. Interacting with some HTTP APIs
  2. Write some network code

and he really cares about

  1. He can finish job in a quick and ergonomic fashion.
  2. Rust is performant for his job.
  3. He has low level control if he wants

NOT rust has one million implementation details that are different from C#, JS. It is a language designer's job to hide irrelevant differences, only exposing the useful ones to the users.

Besides, no one is currently complaining the await!() for reasons like "I don't know its a state machine", "I don't know it's pull based".

Users don't and won't care about these differences.

Awaiting it will drive the future to completion, which is the same as everywhere else.

Well, not quite, is it? If I just write let result = await!(future) in an async fn and call that function, nothing will happen until it is placed on an executor and polled by it.

@ben0x539 yes, I've re-read it... But currently I don't have anything to add.

@CAD97 glad to see my post was inspiring. I can answer about Kotlin: by default it works in the same way as other languages, although you can achieve Rust-like lazy behavior if you want it (e.g. val lazy = async(start=LAZY) { deferred_operation() };). And about similar semantics: IMO the closest semantics is provided in reactive programming, since reactive observables are cold (lazy) by default (e.g. val lazy = Observable.fromCallable { deferred_operation() };). Subscribing to them schedules actual work in the same way as await!() in Rust, but there's also big difference that subscribing by default is non-blocking operation and computed asynchronously results almost always are handled in closures separately from current control flow. And there's a big difference in how cancellation works. So, I think that Rust async behavior is unique, and I fully support your argument that await is confusing and different syntax is only advantage here!

@Pzixel I'm pretty sure that there wouldn't appear any new sigil. Implementing a yield as sigil doesn't make any sense because it's control flow construct like if/match/loop/return. Obviously all control flow constructs should use keywords because they describes business logic and business logic is always in priority. Contrarily, @ as well as ? are exception handling constructs and exception handling logic is less important, therefore it must be subtle. I've not found any strong arguments that having a big await keyword is a good thing (considering amount of text it's possible that I miss them) because all of them appealing to authority or experience (that however not means I dismiss someones authority or experience - it just can't be applied here in full maner).

@tajimaha the Python's example you provided actually has more "implementation details". We can consider for as async, (int i = 0; i < n; i++) as await, and i in range(n) as @ and here it's obvious that Python introduced new keyword - in (in Java it's actually sigil - :) and additionally it introduced a new range syntax. It could reuse something more familiar like (i=0, n=0; i<n; i++) instead of introducing a lot of implementation details. But in current way impact on user experience is only positive, syntax is simpler, and users really care about it.

@Pzixel I'm pretty sure that there wouldn't appear any new sigil. Implementing a yield as sigil doesn't make any sense because it's control flow construct like if/match/loop/return. Obviously all control flow constructs should use keywords because they describes business logic and business logic is always in priority. Contrarily, @ as well as ? are exception handling constructs and exception handling logic is less important, therefore it must be subtle.

await is not an "exception handling construct".
Based on invalid premise your implications are false as well.

exception handling is a control flow as well, but nobody think ? is a bad thing.

I've not found any strong arguments that having a big await keyword is a good thing (considering amount of text it's possible that I miss them) because all of them appealing to authority or experience (that however not means I dismiss someones authority or experience - it just can't be applied _here_ in full maner).

Because Rust don't have its own experience, so it only could see what other languages think about it, others teams experience and so on. You are so confident in sigil, but I'm not sure if you even tried to actually use it.

I do not want to make an argument about syntax in Rust, but I have seen many postfix vs prefix arguments and maybe we can have best of both worlds. And someone else already tried to propose this in C++. C++ and Coroutine TS proposal was mentioned couple of times here, but in my view an alternative proposal named Core Coroutines deserves more attention.

Authors of Core Coroutines propose to replace co_await with new operator-like token.
With, what they call unwrap operator syntax, one is able to use both prefix and postfix notation (without ambiguity):

future​<string>​ g​();
string​ s ​=​​ [<-]​ f​();

optional_struct​[->].​optional_sub_struct​[->].​field

I thought it might be interesting, or at least it will make the discussion more complete.

But Rust really is different from C# or JavaScript (or any other language). So we cannot just blindly copy what other languages do.

Please don't use "difference" as an excuse for your personal inclination towards unorthodox syntax.

How many differences are there? Take any popular language, Java, C, Python, JavaScript, C#, PHP, Ruby, Go, Swift, etc., static, dynamic, compiled, interpreted. They are vastly different in feature set, but they still have much in common for their syntax. Is there a moment you feel any of these language is like 'brainf*ck'?

I think we should focus on delivering different but useful features, not weird syntax.

After reading the thread, I also think the debate is largely over when someone invalidate the urgent need for chaining, someone invalidate the claim that people are annoyed by too many temporary variables.

IMO, you have lost the debate for the needs of postfix syntax. You can only resort to "difference". It is another desperate try. It wastes everyone's time.

@tensorduruk I get the feeling that your words are being a bit too hostile to the other users. Please try to get them in check.


And honestly, if there's many people that are against postfix syntax, then we should just decide on a prefix syntax for now, wait to see how code with the prefix syntax gets written, and then do an analysis to see how much of the code written could benefit from having postfix await. That way we appease everyone that isn't comfortable with a innovative change such as postfix await, while also getting a good pragmatic argument of whether or not to have postfix await.

Worst case scenario with this is if we do all that and come up with a postfix await syntax that is somehow completely better than prefix await. That would lead to lots of code churn if people would be bothered to switch to the better form of await.

And I think that all this syntax discussion really comes down to chaining. If chaining weren't a thing, then postfix await would be completely out the window and it would be a lot easier to settle on just prefix await. However, chaining is very much a thing in Rust, and so it opens up the discussion to the following topics:

if we should have only postfix await:
    what's the best syntax for it that:
         benefits chaining?
         is also ok in non-chaining scenarios
         is readable in both chainable and non-chainable contexts?
else if we should have only prefix await:
    what's the best syntax for it that:
         isn't ambiguous in the sense of order of operation (useful vs obvious)
else if we should have both prefix and postfix await:
    what's the best syntax for it that:
         benefits chaining?
         is also ok in non-chaining scenarios
         is readable in both chainable and non-chainable contexts?
         isn't ambiguous in the sense of order of operation (useful vs obvious)
    should it be a single unified syntax that somehow works for both prefix and postfix?
    would there be clear situations where prefix syntax is favored over postfix?
    would there be a situation where postfix syntax isn't allowed, but prefix is, and vice-versa?

Or something like that. If someone can come up with a better decision pattern with better points than I did, please do so! XD

So first of all, instead of even discussing the syntax, we should decide if we want postfix, prefix, or both postfix and prefix, and why we'd want the choice that we come up with. Once we have that settled, then we can go onto smaller problems regarding syntax choice.

@tensorduruk

IMO, you have lost the debate for the needs of postfix syntax. You can only resort to "difference". It is another desperate try. It wastes everyone's time.

Seriously, why not just use your said languages instead of the uncalled hostility here?
Using your logic, rust should have classes because other conventional language do have it. Rust should not have borrowck because other language do not have it.

Not handling chaining, would be a missed opportunity. i,e I wouldn't want to create bindings for a temporary variable, if chaining can anonymize it away.

However, I feel we should just stick to current macro keyword for await as in nightly. Have postfix macros as a nightly feature and let people toy with it. Yes, there will be churns once we've settled, but that can be handled by rustfix.

@tensorduruk

IMO, you have lost the debate for the needs of postfix syntax. You can only resort to "difference". It is another desperate try. It wastes everyone's time.

Seriously, why not just use your said languages instead of the uncalled hostility here?
Using your logic, rust should have classes because other conventional language do have it. Rust should not have borrowck because other language do not have it.

Not handling chaining, would be a missed opportunity. i,e I wouldn't want to create bindings for a temporary variable, if chaining can anonymize it away.

However, I feel we should just stick to current macro keyword for await as in nightly. Have postfix macros as a nightly feature and let people toy with it. Yes, there will be churns once we've settled, but that can be handled by rustfix.

please read carefully. i said we should provide useful features not weird syntax. struct? borrowck? features!

please also solve problems that really exist. people showed, with examples or stats that temporary bindings may not be a problem. do you guys supporting f await f.await ever try to convince the other side using evidence?

also downvoting me is useless. to get postfix accepted, you need to argue where postfix is useful (probably don't repeat chaining argument, that's a dead end; probably a frequent problem that annoys people, with some evidence, not a toy problem).

can we get the syntax like C# or JS ? most developers use it in the world, I don't like rust use new syntax or Inconsistent, Rust is also hard for new people to learn.

The following would be appendix to my post about using postfix @ insead of await


We should use experience of many other languages instead

And we actually use it. But that not means we must completely repeat the same experience. Also, when arguing in this way against @ you most likely wouldn't achieve any useful result because appealing to previous experience isn't convincible:

Genetic accounts of an issue may be true, and they may help illuminate the reasons why the issue has assumed its present form, but they are not conclusive in determining its merits.

We should use await because many people loves it

Those people may love await for many other reasons, not just because it's await. These reasons might not exist in Rust as well. And arguing in this way against @ most likely wouldn't bring any new points into discussion because appealing to public isn't convincible:

The argumentum ad populum can be a valid argument in inductive logic; for example, a poll of a sizeable population may find that 100% prefer a certain brand of product over another. A cogent (strong) argument can then be made that the next person to be considered will also very likely prefer that brand (but not always 100% since there could be exceptions), and the poll is valid evidence of that claim. However, it is unsuitable as an argument for deductive reasoning as proof, for instance to say that the poll proves that the preferred brand is superior to the competition in its composition or that everyone prefers that brand to the other.

@Pzixel

But rust is another thing. We have ? here and we have to propagate errors. In C# async/await automatically wraps exceptions, throws it across await points and so on. You don't have it in rust. Consider every time you write await in C# you have to write

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

It's very tedious to have all these braces.

In C# you write this:

try
{
  var response = await client.GetAsync("www.google.com");
  var json =  await response.ReadAsStreamAsync();
  var somethingElse = await DoMoreAsyncStuff(json);
}
catch (Exception ex)
{
  // handle exception
}

Regarding the propagate-error-chaining argument, e.g. foo().await?, is there any reason why the ? couldn't be added to the await operator in prefix?

let response = await? getProfile();

Another thing that just occurred to me: what if you want to match on a Future<Result<...>>? Which of these is easier to read?

// Prefix
let userId = match await response {
  Ok(u) => u.id,
  _ => -1
};
// Postfix
let userId = match response {
  Ok(u) => u.id,
  _ => -1
} await;

Additionally, would an async match expression be a thing? As in, would you want the body of a match expression to be async? If so, there would be a difference between match await response and await match response. Because match and await are both effectively unary operators, and match is already prefix, it would be easier to distinguish if await were also prefix. With one prefix and one postfix, it becomes difficult to specify whether you're awaiting the match or the response.

let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await; // Are we awaiting match or response here?

If you have to await both things, you'd be looking at something like

// Prefix - yes, double await is weird and ugly but...
let userId = await match await response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;
// Postfix - ... this is weirder and uglier
let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await await;

Although I guess that might be

// Postfix - ... this is weirder and uglier
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;

(Programming language design is hard.)

Regardless, I'm going to reiterate that Rust has precedence for unary operators being prefix with match, and await is a unary operator.

C# // Postfix - ... this is weirder and uglier let userId = match response await { ... } await;

Beauty is in the eye of the beholder.

Regardless, I'm going to reiterate that Rust has precedence for unary operators being prefix with match, and await is a unary operator.

? on the other hand is unary but postfix.

Regardless, I feel that the discussion is now circling the drain. Without bringing up any new discussion points, there is no use to re-iterate the same positions again and again.

FWIW, I'm happy to get await support whatever the syntax - while I have my own preferences, I don't think any of the realistic suggestions is too terrible to use.

@markrendle I'm not sure what you answering on

In C# you write this:

I know how I write in C#. I said "imagine how could it look it we didn't have exceptions". Because Rust doesn't.

Regarding the propagate-error-chaining argument, e.g. foo().await?, is there any reason why the ? couldn't be added to the await operator in prefix?

It was already discussed twice or trice, please, read the topic. In a nutshell: it's an artificial construct, that won't work well if we will have something additional to ?. await? as suffix just works, when await? as prefix requires additional support in compiler. And it still require braces for chaining (which I peronally dislike here, but people always mention it as important thing), when postfix await doesn't.

Another thing that just occurred to me: what if you want to match on a Future<Result<...>>? Which of these is easier to read?

// Real postfix
let userId = match response await {
  Ok(u) => u.id,
  _ => -1
};
// Real Postfix 2 - looks fine, except it's better to be
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => ok(-1)
} await;
// Real Postfix 2
let userId = match response await {
  Ok(u) => somethingAsync(u) await,
  _ => -1
};

As another user of C# I'll say that its prefix syntaxes: new, await and C-style casts have tripped my intuition the most. I strongly support the postfix operator option.

However, any syntax will be better than chaining futures explicitly, even a pseudo-macro. I will welcome any resolution.

@orthoxerox You raise a very good point. In my dayjob I mostly write Java and I despise the new operator to a level that all my classes which need explicit instantiation (a surprisingly rare occurence when you use builder patterns and dependency injection) have a static factory method just that I can hide the operator.

@Pzixel

@markrendle I'm not sure what you answering on

In C# you write this:

I know how I write in C#. I said "imagine how could it look it we didn't have exceptions". Because Rust doesn't.

I'm guessing this is probably a language barrier thing because that's not at all what you said, but I'll accept it may have been what you meant.

Anyway, as @rolandsteiner said, the important thing is that we get some form of async/await, so I'm happy to await the core team's decision, and all y'all postfix fans can the core team's decision await. 😛 ❤️ ☮️

@yasammez Come to C#. In v8.0 we get to just use new() without the type name :)

I'm just gonna throw out some ideas for the postfix operator.

foo()~; // the pause operator
foo()^^; // the road bumps operator
foo()>>>; // the fast forward operator

Not saying if postfix operator is the way to go or not, but personally I find @ one of the most noisy and weird looking from all possible choices. ~ from the @phaux comment seems much more elegant and less "busy". Also, if I'm not missing anything, we don't use it for anything in Rust yet.

I was proposed ~ before @phaux though I don't want to claim patent ;P

I proposed this because it is like an echo talking:

Hi~~~~~
Where r u~~~~~

Hay~~~~~
I am in another mountain top~~~~~

~ is sometimes used after a sentence to indicate trailing off, which is apt for await!

I can't tell if this thread have hit peak ridiculousness or are we onto something here.

I think ~ it's not easy to answer on some keyboards, especially some small and delicate mechanical keyboards.

Could be:

let await userId = match response {
  Ok(u) => u.id,
  _ => -1
};
let await userId = match response {
  await Ok(u) => somethingAsync(u),
  _ => ok(-1)
};

We could introduce a trigraph like ... for users on keyboard layouts where ~ is inconvenient.

At first, I was strongly on the side of having a delimiter-required prefix syntax as await(future) or await{future} since it is so unambiguous and easy to visually parse. However, I understand the proposals by others that a Rust Future is not like most other Futures from other languages, since it does not put a task onto an executor immediately, but instead is more of a control flow structure which transforms the context into what is homomorphic to a monad call chain essentially.

It makes me think that it is somewhat unfortunate that now there is a confusion in attempting to compare it to other languages with that regard. The closest analog really is the monad do notation in Haskell or the for comprehension in Scala (which are the only ones I'm familiar with off the top of my head). Suddenly, I appreciate the consideration of proposing a unique syntax, but I'm afraid that the existence of the ? operator has both encouraged and discouraged the use of other sigils with it. Any other sigil based operators next to ? make it look noisy and confusing, such as future@?, but the precedent set by having a postfix sigil operator means that another one is not so ridiculous.

I have, therefore, been convinced of the merit of the postfix sigil operator. The downside of this, is that the sigil I would've most preferred is taken by the never type. I would've preferred ! since I think that future!? would make me chuckle whenever I wrote it, and it makes the most visual sense to me to see. I suppose $ would be next, then since it is visually discernable future$?. Seeing ~ still reminds me of the early days of Rust when ~ was the prefix operator for heap allocation. It's all very personal, though, so I don't envy the final decision makers. Were I them, I would probably just go with the inoffensive choice of the prefix operator with required delimiters.

However, I understand the proposals by others that a Rust Future is not like most other Futures from other languages, since it does not put a task onto an executor immediately, but instead is more of a control flow structure which transforms the context into what is homomorphic to a monad call chain essentially.

I tend to disagree. The behaviour you mention is not a property of await, but of the surrounding async function or scope. It's not await that delays the execution of the code preceding it, but the scope that contains said code.

Probably the problem with weirdly looking @ symbol is that using it in that context was never been expected before, therefore in most fonts it's provided in so uncomfortable for us shape.

Then providing a better glyph and some ligatures for popular programming fonts (or at least for Mozilla's Fira Code) might improve situation a bit.

In all other cases, for me it don't looks that @ is so weird to cause any real problems when writing or maintaining code.


E.g. the following code uses different than @ symbol - :


// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)@? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()@?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()@?
    .error_for_status()?
    .json()@?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()@?
    .error_for_status()?
    .json()@?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)@?
        .res.json::<UserResponse>()@?
        .user
        .into();

    Ok(user)
}

Expand for comparison how it looks with regular ANSI @


// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)@? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()@?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()@?
    .error_for_status()?
    .json()@?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()@?
    .error_for_status()?
    .json()@?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)@?
        .res.json::<UserResponse>()@?
        .user
        .into();

    Ok(user)
}

@norcalli

At first, I was strongly on the side of having a delimiter-required prefix syntax as await(future) or await{future} since it is so unambiguous and easy to visually parse.

Then you probably want unambiguous if { cond }, while { cond } and match { expr }...

However, I understand the proposals by others that a Rust Future is not like most other Futures from other languages

It's not true. It's actually is like most other futures from other languages. Difference between "run until first await when spawned" vs "run until first await when polled" is not that big. I know because I worked with both. If you actually think "when this difference comes to play" you will find only corner case. e.g. you will get error when creating future on first poll instead of when it's created.

They could be internally different, but they are quite the same from user perspective so it doesn't make sense to me to make a distinction here.

@Pzixel

Difference between "run until first await when spawned" vs "run until first await when polled" is not that big.

That's not the difference we're talking about, though. We're not talking about lazy vs eager -to-first-await.

What we're talking about is that await joins the awaited Promise (JS) / Task (C#) on the executor in other languages, which was already put on the executor at construction (so was already running in the "background"), but in Rust, Futures are inert state machines until await!-driven.

Promise/Task is a handle to a running asynchronous operation. Future is a deferred asynchronous computation. People, including notable names in Rust, have made this mistake before, and examples have been linked before in the middle of these 500+ comments.

Personally, I think this mismatch of semantics is large enough to counteract the familiarity of await. Our Futures accomplish the same goal as Promise/Task, but though a different mechanism.


Anecdotally, for me when I first learned async/await in JavaScript, async was "just" something I wrote to get the await superpower. And the way I was taught to get parallelism was a = fa(); b = fb(); /* later */ await [a, b]; (or whatever it is, it's been an age since I've had to write JS). My posit is that other people's view of async lines up with me, in that Rust's semantics don't mismatch on async (give you await superpower), but on Future construction and await!.


At this point, I believe the discussion on the differences in Rust's async/Future/await semantics has run its course, and no new information is being presented. Unless you have a new position and/or insight to bring, it'd probably be best for the thread if we leave that discussion here. (I'd be happy to take it to Internals and/or Discord.)

@CAD97 yes, I see your position, but I think the discinction is not that big.

You got me, I got you. So let the discussion have its flow.

@CAD97

People, including notable names in Rust, have made this mistake before, and examples have been linked before in the middle of these 500+ comments.

If even people intimately familiar with Rust make that mistake, is it really a mistake?

So, we had a number of discussions about async-await at the Rust All Hands. In the course of those discussions, a few things became clear:

First, there is no consensus (yet) in the lang team about the await syntax. There are clearly a lot of possibilities and strong arguments in favor of all of them. We spent a long time exploring alternatives and produced quite a lot of interesting back-and-forth. An immediate next step for this discussion, I think, is to convert those notes (along with other comments from this thread) into a kind of summary comment that lays out the case for each variant, and then to continue from there. I'm working with @withoutboats and @cramertj on that.

Stepping back from the syntax question, another thing that we plan to do is an overall triage of the status of the implementation. There are a number of current limitations (e.g., the implementation requires TLS under the hood presently to thread information about the waker). These may or may not be blockers to stabilization, but regardless they are issues that do need to ultimately be addressed, and that is going to require some concerted effort (in part from the compiler team). Another next step then is to conduct this triage and generate a report. I expect us to conduct this triage next week, and we'll have an update then.

In the meantime, I am going to go ahead and lock this thread until we've had a chance to produce the above reports. I feel like the thread has already served its purpose of exploring the possible design space in some depth and further comments aren't going to be particularly helpful as this stage. Once we have the aforementioned reports in hand, we will also lay out the next steps towards reaching a final decision.

(To expand a bit on the final paragraph, I am pretty interested in exploring alternative ways to explore the design space beyond long discussion threads. This is a much bigger topic than I can address in this comment, so I won't go into details, but suffice to say for now that I am quite interested in trying to find better ways to resolve this -- and future! -- syntax debates.)

Async-await status report:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

Relative to my previous comment, this contains the results of the triage and some thoughts about syntax (but not yet a full syntax write-up).

Marking this issue as blocking for async-await stabilization, at least for the time being.

Quite a while ago, Niko promised that we would write a summary of the discussion in the language team and the community about the final syntax for the await operator. Our apologies for the long wait. A write up of the status of the discussion is linked below. Before that, though, let me also give an update on where the discussion stands now and where we will go from here.

Brief summary of where async-await stands right now

First, we hope to stabilize async-await in the 1.37 release , which branches on July 4th, 2019. Since we do not want to stabilize the await! macro, we have to resolve the syntax question before then. Note that this stabilization doesn’t represent the end of the road — more the beginning. There remains feature work to be done (e.g., async fn in traits) and also impl work (continued optimization, bug-fixing, and the like). Still, stabilizing async/await will be a major milestone!

As far as the syntax goes, the plan for resolution is as follows:

  • To start, we are publishing a write-up of the syntax debate thus far -- please take a look.
  • We want to be forward compatible with obvious future extensions to the syntax: processing streams with for loops in particular (like JavaScript's for await loop). That's why I've been working on a series of posts about this issue (first post here and more coming in the future).
  • At the upcoming lang-team meeting on May 2, we plan to discuss the interaction with for loops and also to establish a plan for reaching a final decision on the syntax in time to stabilize async/await in 1.37. We'll post an update after the meeting to this internals thread.

The writeup

The writeup is a dropbox paper document, available here. As you'll see, it is fairly long and lays out a lot of the arguments back-and-forth. We would appreciate feedback on it; rather than re-opening this issue (which already has more than 500 comments) I've created an internals thread for that purpose.

As I said before, we plan to reach a final decision in the near future. We also feel the discussion has largely reached a stable state: expect the next few weeks to be the "final comment period" for this syntax discussion. After the meeting we'll hopefully have a more detailed timeline to share for how this decision will be made.

Async/await syntax is probably the most hotly anticipated feature Rust has gained since 1.0, and the syntax for await in particular has been one of the decisions on which we have received the most feedback. Thank you to everyone who has participated in these discussions over the last few months! This is a choice on which many people have strongly divergent feelings; we want to assure everyone that your feedback is being heard and the final decision will be reached after much thoughtful and careful deliberation.

Was this page helpful?
0 / 5 - 0 ratings