New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve `await` syntax #57640

Open
cramertj opened this Issue Jan 15, 2019 · 509 comments

Comments

Projects
None yet
@cramertj
Copy link
Member

cramertj commented Jan 15, 2019

Before commenting in this thread, please check #50547 and try to check that you're not duplicating arguments that have already been made there.


Notes from shepherds:

If you're new to this thread, consider starting from #57640 (comment), which was followed by three great summary comments, the latest of which were #57640 (comment). (Thanks, @traviscross!)

@mehcode

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 15, 2019

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

This comment has been minimized.

Copy link
Contributor

lnicola commented Jan 15, 2019

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

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 15, 2019

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

This comment has been minimized.

Copy link

earthengine commented Jan 15, 2019

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

This comment has been minimized.

Copy link

kvinwang commented Jan 16, 2019

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

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 16, 2019

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

This comment has been minimized.

Copy link
Member

scottmcm commented Jan 16, 2019

@cramertj Since there are 276 comments in #50547, could you summarize the arguments made there to make it easier to not repeat them here? (Maybe add them to the OP here?)

@chpio

This comment has been minimized.

Copy link
Contributor

chpio commented Jan 16, 2019

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

This comment has been minimized.

Copy link

HeroicKatora commented Jan 16, 2019

One reason to be in favour of a post-fix syntax is that from the callers perspective, an await behaves a lot like a function call: You relinquish flow control and when you get it back a result is waiting on your stack. In any case, I'd prefer a syntax that embraces the function-like behaviour by containing function-paranthesis. And there are good reasons to want to split construction of coroutines from their first execution so that this behaviour is consistent between sync and async blocks.

But while the implicit coroutine style has been debated, and I'm on the side of explicitness, could calling a coroutine not be explicit enough? This probably works best when the coroutine is not directly used where constructed (or could work with streams). In essence, in contrast to a normal call we expect a coroutine to take longer than necessary in a more relaxed evaluation order. And .await!() is more or less an attempt to differentiate betwen normal calls and coroutine calls.

So, after hopefully having provided a somewhat new take on why post-fix could be preferred, a humble proposal for syntax:

  • future(?)
  • or future(await) which comes with its own tradeoffs of course but seems to be accepted as less confusing, see bottom of post.

Adapting a fairly popular example from other thread (assuming the logger.log to also be a coroutine, to show what immediately calling looks like):

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

And with the alternative:

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

To avoid unreadable code and to help parsing, only allow spaces after the question mark, not between it and the open paran. So future(? ) is good while future( ?) would not be. This issues does not arise in the case of future(await) where all current token can be used as previously.

The interaction with other post-fix operators (such as the current ?-try) is also just like in function calls:

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = acquire_lock()(?);
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.log_into(message)(?)?;
    logger.timestamp()(?);
    Ok(length)
}

Or

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = acquire_lock()(await);
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.log_into(message)(await)?;
    logger.timestamp()(await);
    Ok(length)
}

A few reasons to like this:

  • Compared to .await!() it does not allude to a member that could have other uses.
  • It follows natural precedence of calls, such as chaining and use of ?. This keeps the number of precendence classes lower and helps with learning. And function calls have always been somewhat special in the language (even though the have a trait), so that there is no expectation of user code being able to define their own my_await!() that has very similar syntax and effect.
  • This could generalize to generators and streams, as well as generators that expect more arguments to be provided on resumption. In essence, this behaves as an FnOnce while Streams would behave like a FnMut. Additional arguments may also be accomodated easily.
  • For those who have used current Futures before, this captures how a ? with Poll should have worked all along (unfortunate stabilization sequence here). As a learning step, it is also consistent with expecting a ? based operator to divert control flow. (await) on the other hand would not satisfy this but afterall the function will always expect to resume at the divergent point.
  • It does use a function-like syntax, though this argument is only good if you agree with me 😄

And reasons not to like this:

  • ? appears to be an argument but it is not even applied to an expression. I believe this could be solved through teaching, as the token it appears to be applied to is the function call itself, which is the somewhat correct notion. This also positively means that the syntax is unambiguous, I hope.
  • More (and different) mix of paranthesis and ? can difficult to parse. Especially when you have one future returning a result of another future: construct_future()(?)?(?)?. But you could make the same argument for being able to a result of an fn object, leading to expression such as this being allowed: foobar()?()?()?. Since nevertheless I've never seen this used nor complaint, splitting into separate statements in such cases seems to be required rarely enough. This issues also does not exist for construct_future()(await)?(await)?-
  • future(?) is my best shot at a a terse and still somewhat concise syntax. Yet, its reasoning is grounded on implementation details in coroutines (temporarily returning and dispatching on resume), which might make it unsuitable for an abstraction. future(await) would be an alternative that could still be explainable after await has been internalized as a keyword but the argument position is a bit hard to swallow for me. It could be fine, and it is certainly more readable when the coroutine returns a result.
  • Interference with other function call proposals?
  • Your own? You need not like it, it just felt like a waste to not at least propose this terse post-fix syntax.
@Pauan

This comment has been minimized.

Copy link
Member

Pauan commented Jan 17, 2019

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

This comment has been minimized.

Copy link

HeroicKatora commented Jan 17, 2019

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

This comment has been minimized.

Copy link
Member

Pauan commented Jan 17, 2019

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

This comment has been minimized.

Copy link

HeroicKatora commented Jan 17, 2019

If it's specifically the hint to operator ? which is heavy, one could of course replace it with the newly reserved keyword. I had only initially considered that this feel too much like an actual argument of puzzling type but the tradeoff could work in terms of helping mentally parse the statement. So the same statement for impl Future<Output = Result<_,_>> would become:

  • foo()(await)?

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

@chpio

This comment has been minimized.

Copy link
Contributor

chpio commented Jan 17, 2019

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

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Jan 17, 2019

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

This comment has been minimized.

Copy link

HeroicKatora commented Jan 17, 2019

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

This comment has been minimized.

Copy link
Contributor

withoutboats commented Jan 17, 2019

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

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


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

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

  • Prefix await with mandatory delimiters. Here this is also a decision of what delimiters (either braces or parens or accepting both; all of these have their own pros and cons). That is, await(future) or await { future }. This completely solves the precedence problems, but is syntactically noisy and both delimiter options present possible sources of confusion.
  • Prefix await with the "useful" precedence regarding ?. (That is, that await binds tighter than ?). This may surprise some users reading code, but I believe functions that return futures of results will be overwhelmingly more common than functions that return results of futures.
  • Prefix await with the "obvious" precedence regarding ?. (That is, that ? binds tighter than await). Additional syntax sugar await? for a combined await and ? operator. I think this syntax sugar is necessary to make this precedence order viable at all, otherwise everyone will be writing (await future)? all the time, which is a worse variant of the first option I enumerated.
  • Postfix await with the syntax space await. This solves the precedence problem by having a clear visual ordering between the two operators. I feel uneasy about this solution in a lot of respects.

My own ranking amonst these choices changes every time I examine the issue. As of this moment, I think using the obvious precedence with the sugar seems like the best balance of ergonomics, familiarity, and comprehension. But in the past I've favored either of the two other prefix syntaxes.

For the sake of discussion, I'll give these four options these names:

Name Future Future of Result Result of Future
Mandatory delimiters await(future) or await { future } await(future)? or await { future }? await(future?) or await { future? }
Useful precedence await future await future? await (future?)
Obvious precedence w/ sugar await future await? future or (await future)? await future?
Postfix keyword future await future await? future? await

(I've specifically used "postfix keyword" to distinguish this option from other postfix syntaxes like "postfix macro".)

@HeroicKatora

This comment has been minimized.

Copy link

HeroicKatora commented Jan 17, 2019

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

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Jan 17, 2019

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

This comment has been minimized.

Copy link

HeroicKatora commented Jan 17, 2019

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

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 17, 2019

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

This comment has been minimized.

Copy link
Contributor

crlf0710 commented Jan 18, 2019

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

This comment has been minimized.

Copy link

mzji commented Jan 18, 2019

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

This comment has been minimized.

Copy link
Contributor

chpio commented Jan 18, 2019

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

@melodicstream

This comment has been minimized.

Copy link

melodicstream commented Feb 10, 2019

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

@jptbaba

This comment has been minimized.

Copy link

jptbaba commented Feb 10, 2019

@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

This comment has been minimized.

Copy link

tensorduruk commented Feb 10, 2019

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

@luover

This comment has been minimized.

Copy link

luover commented Feb 10, 2019

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.

@I60R

This comment has been minimized.

Copy link

I60R commented Feb 10, 2019

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.

@markrendle

This comment has been minimized.

Copy link

markrendle commented Feb 10, 2019

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

@markrendle

This comment has been minimized.

Copy link

markrendle commented Feb 10, 2019

Regardless, I'm going to reiterate that Rust has precedence for unary operators being prefix with match, and await is a unary operator.

@rolandsteiner

This comment has been minimized.

Copy link

rolandsteiner commented Feb 10, 2019

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

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Feb 10, 2019

@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
};
@orthoxerox

This comment has been minimized.

Copy link

orthoxerox commented Feb 10, 2019

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.

@yasammez

This comment has been minimized.

Copy link

yasammez commented Feb 10, 2019

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

@markrendle

This comment has been minimized.

Copy link

markrendle commented Feb 10, 2019

@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. 😛 ❤️ ☮️

@markrendle

This comment has been minimized.

Copy link

markrendle commented Feb 10, 2019

@yasammez Come to C#. In v8.0 we get to just use new() without the type name :)

@phaux

This comment has been minimized.

Copy link

phaux commented Feb 12, 2019

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

This comment has been minimized.

Copy link
Contributor

dpc commented Feb 12, 2019

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.

@earthengine

This comment has been minimized.

Copy link

earthengine commented Feb 13, 2019

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~~~~~
@casey

This comment has been minimized.

Copy link
Contributor

casey commented Feb 13, 2019

~ is sometimes used after a sentence to indicate trailing off, which is apt for await!

@dpc

This comment has been minimized.

Copy link
Contributor

dpc commented Feb 13, 2019

I can't tell if this thread have hit peak ridiculousness or are we onto something here.

@wq7

This comment has been minimized.

Copy link

wq7 commented Feb 14, 2019

I think ~ it's not easy to answer on some keyboards, especially some small and delicate mechanical keyboards.

@superriva

This comment has been minimized.

Copy link

superriva commented Feb 14, 2019

Could be:

let await userId = match response {
  Ok(u) => u.id,
  _ => -1
};
let await userId = match response {
  await Ok(u) => somethingAsync(u),
  _ => ok(-1)
};
@ben0x539

This comment has been minimized.

Copy link
Contributor

ben0x539 commented Feb 14, 2019

We could introduce a trigraph like ... for users on keyboard layouts where ~ is inconvenient.

@norcalli

This comment has been minimized.

Copy link
Contributor

norcalli commented Feb 14, 2019

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.

@lnicola

This comment has been minimized.

Copy link
Contributor

lnicola commented Feb 14, 2019

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.

@I60R

This comment has been minimized.

Copy link

I60R commented Feb 15, 2019

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

This comment has been minimized.

Copy link

Pzixel commented Feb 15, 2019

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

@CAD97

This comment has been minimized.

Copy link
Contributor

CAD97 commented Feb 15, 2019

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

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Feb 15, 2019

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

@orthoxerox

This comment has been minimized.

Copy link

orthoxerox commented Feb 15, 2019

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

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Feb 15, 2019

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

@rust-lang rust-lang locked and limited conversation to collaborators Feb 15, 2019

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.