Skip to content
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

Closed
cramertj opened this issue Jan 15, 2019 · 512 comments
Closed

Resolve await syntax #57640

cramertj opened this issue Jan 15, 2019 · 512 comments

Comments

@cramertj
Copy link
Member

@cramertj 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
Copy link
Contributor

@mehcode 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
Copy link
Contributor

@lnicola 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
Copy link
Contributor

@mehcode 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
Copy link

@earthengine 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
Copy link

@kvinwang 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
Copy link
Contributor

@mehcode 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
Copy link
Member

@scottmcm 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
Copy link
Contributor

@chpio 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
Copy link
Contributor

@HeroicKatora 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
Copy link

@Pauan 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
Copy link
Contributor

@HeroicKatora 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
Copy link

@Pauan 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
Copy link
Contributor

@HeroicKatora 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
Copy link
Contributor

@chpio 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
Copy link
Member

@Nemo157 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
Copy link
Contributor

@HeroicKatora 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
Copy link
Contributor

@withoutboats 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
Copy link
Contributor

@HeroicKatora 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
Copy link
Member

@Nemo157 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
Copy link
Contributor

@HeroicKatora 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
Copy link
Contributor

@mehcode 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
Copy link
Member

@crlf0710 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
Copy link

@mzji 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
Copy link
Contributor

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

@markrendle
Copy link

@markrendle 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
Copy link

@markrendle 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
Copy link

@rolandsteiner 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
Copy link

@Pzixel 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
Copy link

@orthoxerox 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
Copy link

@yasammez 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
Copy link

@markrendle 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
Copy link

@markrendle markrendle commented Feb 10, 2019

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

@phaux
Copy link

@phaux 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
Copy link
Contributor

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

@earthengine 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
Copy link
Contributor

@casey casey commented Feb 13, 2019

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

@dpc
Copy link
Contributor

@dpc dpc commented Feb 13, 2019

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

@wjdnsdhydl
Copy link

@wjdnsdhydl wjdnsdhydl commented Feb 14, 2019

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

@aleksmelnikov
Copy link

@aleksmelnikov aleksmelnikov 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
Copy link
Contributor

@ben0x539 ben0x539 commented Feb 14, 2019

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

@norcalli
Copy link
Contributor

@norcalli 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
Copy link
Contributor

@lnicola 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
Copy link

@I60R 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
Copy link

@Pzixel 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
Copy link
Contributor

@CAD97 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
Copy link

@Pzixel 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
Copy link

@orthoxerox 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
Copy link
Contributor

@nikomatsakis 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
@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Mar 1, 2019

Async-await status report:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

Relative to my previous comment, this contains the results of the triage and some thoughts about syntax (but not yet a full syntax write-up).

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Mar 5, 2019

Marking this issue as blocking for async-await stabilization, at least for the time being.

@withoutboats
Copy link
Contributor

@withoutboats withoutboats commented Apr 29, 2019

Quite a while ago, Niko promised that we would write a summary of the discussion in the language team and the community about the final syntax for the await operator. Our apologies for the long wait. A write up of the status of the discussion is linked below. Before that, though, let me also give an update on where the discussion stands now and where we will go from here.

Brief summary of where async-await stands right now

First, we hope to stabilize async-await in the 1.37 release , which branches on July 4th, 2019. Since we do not want to stabilize the await! macro, we have to resolve the syntax question before then. Note that this stabilization doesn’t represent the end of the road — more the beginning. There remains feature work to be done (e.g., async fn in traits) and also impl work (continued optimization, bug-fixing, and the like). Still, stabilizing async/await will be a major milestone!

As far as the syntax goes, the plan for resolution is as follows:

  • To start, we are publishing a write-up of the syntax debate thus far -- please take a look.
  • We want to be forward compatible with obvious future extensions to the syntax: processing streams with for loops in particular (like JavaScript's for await loop). That's why I've been working on a series of posts about this issue (first post here and more coming in the future).
  • At the upcoming lang-team meeting on May 2, we plan to discuss the interaction with for loops and also to establish a plan for reaching a final decision on the syntax in time to stabilize async/await in 1.37. We'll post an update after the meeting to this internals thread.

The writeup

The writeup is a dropbox paper document, available here. As you'll see, it is fairly long and lays out a lot of the arguments back-and-forth. We would appreciate feedback on it; rather than re-opening this issue (which already has more than 500 comments) I've created an internals thread for that purpose.

As I said before, we plan to reach a final decision in the near future. We also feel the discussion has largely reached a stable state: expect the next few weeks to be the "final comment period" for this syntax discussion. After the meeting we'll hopefully have a more detailed timeline to share for how this decision will be made.

Async/await syntax is probably the most hotly anticipated feature Rust has gained since 1.0, and the syntax for await in particular has been one of the decisions on which we have received the most feedback. Thank you to everyone who has participated in these discussions over the last few months! This is a choice on which many people have strongly divergent feelings; we want to assure everyone that your feedback is being heard and the final decision will be reached after much thoughtful and careful deliberation.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet