rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
94.76k stars 12.21k forks source link

Resolve `await` syntax #57640

Closed cramertj closed 5 years ago

cramertj commented 5 years ago

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

mehcode commented 5 years ago

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.

lnicola commented 5 years ago

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.

mehcode commented 5 years ago

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.

earthengine commented 5 years ago

I would like to see that this issue was refered in the top post of #50547 (beside the check box "Final syntax for await.").

kvinwang commented 5 years ago

Kotlin

val result = task.await()

Kotlin's syntax is:

val result = doTask()

The await is just a suspendable function, not a first-class thing.

mehcode commented 5 years ago

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.

scottmcm commented 5 years ago

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

chpio commented 5 years ago

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?

HeroicKatora commented 5 years ago

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:

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:

And reasons not to like this:

Pauan commented 5 years ago

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?

HeroicKatora commented 5 years ago

It seems it was not clear what I meant. future(?) is what was previously discussed as future.await!() or similar. Branching on a future that returns a result as well would be future(?)? (two different ways how we can relinquish control flow early). This makes future-polling (?) and result testing? orthogonal. Edit: added an extra example for this.

Pauan commented 5 years ago

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.

HeroicKatora commented 5 years ago

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:

The best argument why ? is appropriate is that the internal mechanism used is somewhat similar (otherwise we couldn't use Poll in current libraries) but this may miss the point of being a good abstraction.

chpio commented 5 years ago

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.

Nemo157 commented 5 years ago

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.

HeroicKatora commented 5 years ago

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.

withoutboats commented 5 years ago

How many times do you expect to see -> impl Future<Output = Result<, >> in an async codebase?

I expect to see the type equivalent of this (an async fn that returns a Result) all the time, likely even the majority of all async fns, since if what you're awaiting is an IO even, you'll almost certainly be throwing IO errors upwards.


Linking to my previous post on the tracking issue and adding a few more thoughts.

I think there's very little chance a syntax that does not include the character string await will be accepted for this syntax. I think at this point, after a year of work on this feature, it would be more productive to try to weigh the pros and cons of the known viable alternatives to try to find which is best than to propose new syntaxes. The syntaxes I think are viable, given my previous posts:

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

HeroicKatora commented 5 years ago

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);
}
Nemo157 commented 5 years ago
// 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).

HeroicKatora commented 5 years ago

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

However, under 'Useful precedence' and await expr? (await expr yields T) we instead get

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)
mehcode commented 5 years ago

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?;
crlf0710 commented 5 years ago

Actually i think every separator looks fine for postfix syntax, for example: let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?; But i don't have a strong opinion about which one to use.

mzji commented 5 years ago

Could postfix macro (i.e. future.await!()) still be an option? It's clear, concise, and unambiguous:

Future Future of Result Result of Future
future.await!() future.await!()? future?.await!()

Also postfix macro requires less effort to be implemented, and is easy to understand and use.

chpio commented 5 years ago

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

novacrazy commented 5 years ago

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

Nemo157 commented 5 years ago

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

novacrazy commented 5 years ago

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

HeroicKatora commented 5 years ago

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

HeroicKatora commented 5 years ago

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.

novacrazy commented 5 years ago

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

cramertj commented 5 years ago

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.

HeroicKatora commented 5 years ago

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())?;
HeroicKatora commented 5 years ago

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.

yazaddaruvala commented 5 years ago

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?;
mehcode commented 5 years ago

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

yazaddaruvala commented 5 years ago

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

lholden commented 5 years ago

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)

norcalli commented 5 years ago

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.

AaronFriel commented 5 years ago

My two cents (not a regular contributor, fwiw) I'm most partial to copying the model of try!. It's been done once before, it worked well and after it became very popular there were enough users to consider a postfix operator.

So my vote would be: stabilize with await!(...) and punt on a postfix operator for nice chaining based on a poll of Rust developers. Await is a keyword, but the ! indicates that it's something "magic" to me and the parenthesis keep it unambiguous.

Also a comparison:

Postfix Expression
.await client.get("https://my_api").send().await?.json().await?
.await! client.get("https://my_api").send().await!?.json().await!?
.await() client.get("https://my_api").send().await()?.json().await()?
^ client.get("https://my_api").send()^?.json()^?
# client.get("https://my_api").send()#?.json()#?
@ client.get("https://my_api").send()@?.json()@?
$ client.get("https://my_api").send()$?.json()$?

My third cent is that I like @ (for "await") and # (to represent multi-threading/concurrency).

casey commented 5 years ago

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.

darkwater commented 5 years ago

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.

ivandardi commented 5 years ago

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.

unraised commented 5 years ago

The postfix keyword option is a clear winner to me.

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()..?
jonimake commented 5 years ago

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.

EyeOfPython commented 5 years ago

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:

syntax fam obv vrb vis cha grp fwd
await!(fut) ++ + -- ++ -- 0 ++
await { fut } ++ ++ -- ++ -- 0 +
await fut ++ - + ++ - 0 -
fut.await 0 -- + ++ ++ + -
fut.await() 0 -- - ++ ++ + -
fut.await!() 0 0 -- ++ ++ + -
fut(await) - -- 0 ++ ++ + -
fut await -- -- + ++ ++ -- -
fut@ - - ++ -- ++ ++ --
fut@await - 0 + ++ ++ ++ 0
jsdw commented 5 years ago

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

josalhor commented 5 years ago

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

nikonthethird commented 5 years ago

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?

I think the second case is much more common when dealing with typical scenarios, since I/O operations might fail. Therefore:

await future? <=> (await future)?

In the less common first case, it is acceptable to have parentheses: await (future?). This might even be a good use for the try! macro if it hadn't been deprecated: await try!(future). This way, the await and question mark operator are not on different sides of the future.

I60R commented 5 years ago

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.

ejmahler commented 5 years ago

C++ (Concurrency TR)

auto result = co_await task;

This is in the Coroutines TS, not concurrency.