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

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Jan 30, 2019

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 30, 2019

@Hirevo

You effectively rewritten my code example but you removed the chaining part of it (by doing bindings).

I don't get this "chaining for chaining" approach. Chaining is good when it's good, e.g. it doesn't make to create useless temp variables. You say "you removed chaining", but you can see that it provides no value here.

For instance, for the debugging part, the following has the exact same behavior with just the necessary dbg! and nothing more (not even extra parentheses)

No, you did this

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = dbg!(serde_json::from_str(dbg!(user)));
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(dbg!(permissions));
    Ok(user)
}

It's not the same thing.

I just said that combinators can play very nicely with await, and that the chaining problem can be addressed by only awaiting the whole expression built by combinators (removing the need for await (await fetch("test")).json() or await { await { fetch("test") }.json() }).

It's only a problem for prefix await, it doesn't exist for other forms of it.

I don't know how to do the same without combinators and no additional bindings or parentheses.

Why not create these bindings? Bindings are always better for reader, for instance, and if you have a debugger which could print a binding value, but which couldn't evaluate an expression.

Finally, you didn't actually removed any bindings. You just used labmda |user| syntax where I did let user = .... It didn't save you anything, but now this code is harder to read, harder to debug, and it doesn't have access to the parent (e.g. you have to wrap errors externally in method chain instead of doing it it call itself, probably).


In a nutshell: chaining doesn't provide value by itself. It may be useful in some scenarios, but it's not one of them. And since I write async/await code for more than six years, I do believe you don't want write chained async code ever. Not because you couldn't, but because it's inconvenient and binding-approach is always nicer to read and often to write. Not to say that you don't need combinators at all. If you have async/await, you don't need these hundreds of methods on futures/streams, you only need two: join and select. Everything else may be done via iterators/bindings/..., i.e. common language tools, that don't make you to learn yet another infrastructure.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 30, 2019

@Sphericon AFAIK community agreed on using await as either keyword or a sigil, so imho your ideas about async require another RFC.

My second comment is that await keyword in Rust does completely different things from await in other language and this has the potential to cause confusion and unexpected race conditions.

Can you be more detailed? I didn't see any difference between JS/C# await's, except futures being poll-based, but it have really nothing to deal with async/await.

@rolandsteiner

This comment has been minimized.

Copy link

rolandsteiner commented Jan 30, 2019

Regarding the await? prefix syntax proposed in several places here:

let foo = await? bar_async();

How would this look with futures of futures result of results *) ? I.e., would it be arbitrarily extensible:

let foo = await?? double_trouble();

IOW, prefix await? looks like a syntax that is too special-cased to me.

*) edited. Thanks to @Nemo157 for pointing it out.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Jan 30, 2019

How would this look with futures of futures? I.e., would it be arbitrarily extensible:

let foo = await?? double_trouble();

IOW, await? looks like a syntax that is too special-cased to me.

@rolandsteiner by "futures of futures" do you mean impl Future<Output = Result<Result<_, _>, _>> (one await + two ? implies "unwrapping" a single future and two results to me, not awaiting nested futures).

await? is a special-case, but it's a special-case that will likely apply to 90%+ uses of await. The entire point of futures is as a way to wait on asynchronous IO operations, IO is fallible so 90%+ of async fn will likely return io::Result<_> (or some other error type that includes an IO variant). Functions that return Result<Result<_, _>, _> are pretty rare currently, so I wouldn't expect them to require special-case syntax.

@rolandsteiner

This comment has been minimized.

Copy link

rolandsteiner commented Jan 30, 2019

@Nemo157 You're right of course: Result of Results. Updated my comment.

@earthengine

This comment has been minimized.

Copy link

earthengine commented Jan 30, 2019

Today, we write

  1. await!(future?) for future: Result<Future<Output=T>,E>
  2. await!(future)? for future: Future<Output=Result<T,E>>

And if we write await future? we have to work out which one it means.

But is it the case that case 1 can always turned into case 2? In case 1, the expression either produce a future, or an error. But the error can be delayed and moved inside the future. So we can just handle case 2 and make an automatic convertsion happening here.

In the programmer's point of view, Result<Future<Output=T>,E> garantees early return for the error case, but except that the two have the same sementic. I can image the compiler can work out and avoid the additional poll call if the error case is immidiate.

So the proposal is:

await exp? can be interpreted as await (exp?) if exp is Result<Future<Output=T>,E>, and interpreted as (await exp)? if exp is Future<Output=Result<T,E>>. In both cases, it will early return in error, and resolve to the true result if running ok.

For more complicated cases, we can apply something like the automatic method receiver dereference:

When interperting await exp???? we first check exp and if is Result, try it and keep going when the result is still a Result until running out of ? or have something that is not Result. Then it have to be a future and we await on it and apply the rest ?s.


I was a postfix keyword/sigil supporter and still am. However I just want to show that the prefix precedence may not be a big issue in pratice and have workarounds.

I know Rust team members don't like implicit things, but in such a case, it is just have too little difference between the potential sementics and we have a good way to ensure we do the right thing.

@Systemcluster

This comment has been minimized.

Copy link

Systemcluster commented Jan 30, 2019

await? is a special-case, but it's a special-case that will likely apply to 90%+ uses of await. The entire point of futures is as a way to wait on asynchronous IO operations, IO is fallible so 90%+ of async fn will likely return io::Result<_> (or some other error type that includes an IO variant). Functions that return Result<Result<_, _>, _> are pretty rare currently, so I wouldn't expect them to require special-case syntax.

Special cases are bad to compose, expand, or learn, and eventually turn into baggage. It's not a good compromise to make exceptions to the rules of the language for a single theoretical usability use-case.

@ivandardi

This comment has been minimized.

Copy link
Contributor

ivandardi commented Jan 30, 2019

Would it be possible to implement Future for Result<T, E> where T: Future? That way you could just await result_of_future without needing to unwrap it with ?. And that of course would return a Result, so you'd call it await result_of_future, which would mean (await result_of_future)?. That way we wouldn't need the await? syntax and the prefix syntax would be a bit more consistent. Let me know if there's anything wrong with this.

@llambda

This comment has been minimized.

Copy link

llambda commented Jan 30, 2019

Additional arguments for await with mandatory delimiters include (personally I'm not sure which syntax I like best overall):

  • No special casing of the ? operator, no await? or await??
  • Congruent with existing control-flow operators such as loop, while, and for, which also require mandatory delimiters
  • Feels most at home to similar existing Rust constructs
  • Eliminating special casing helps avoid trouble when writing macros
  • Doesn't use sigils or postfix, avoiding spending from the strangeness budget

Example:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

However, after playing around with this in an editor, it feels cumbersome still. I think I would rather have await and await? without delimiters, like with break and return.

@mehcode

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 30, 2019

Would it be possible to implement Future for Result<T, E> where T: Future?

You would want the inverse. The most common awaitable is a Future where it's output type is a Result.

There is then the explicit argument agaisnt hiding or otherwise absorbing the ? into just await. What if you want to match on the result, etc.

@ivandardi

This comment has been minimized.

Copy link
Contributor

ivandardi commented Jan 30, 2019

If you have a Result<Future<Result<T, E2>>, E1>, awaiting it would return a Result<Result<T, E2>, E1>.

If you have a Future<Result<T, E1>>, then awaiting it would simply return the Result<T, E1>.

There's no hiding or absorbing the ? into the await, and you can do whatever is needed with the Result afterwards.

@mehcode

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 30, 2019

Oh. I must have misunderstood you then. I don't see how that helps though as we still need to combine the ? with await 99% of the time.


Oh. The await? syntax is supposed to imply (await future)? which would be the common case.

@ivandardi

This comment has been minimized.

Copy link
Contributor

ivandardi commented Jan 30, 2019

Exactly. So we'd just make the await bind tighter in await expr?, and if that expression is a Result<Future<Result<T, E2>>, E1> then it would evaluate to something of the type Result<T, E2>. It would mean there is no special casing for awaiting on Result types. It just follows the normal trait implementations.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 30, 2019

@ivandardi what's about Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@mehcode

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 30, 2019

@Pzixel Note, that form of future is gone. There is a single associated type now, Output (which probably will be a Result).


@ivandardi Okay. I see it now. The only thing you'd have against you there is the precedence being something strange you'd have to learn there as it's a deviation but so is most of anything with await I suppose.

Though a Result that returns a future is so rare I haven't found one case apart from something in tokio core that was removed so I don't think we need any sugar/trait impls to help in that case.

@ivandardi

This comment has been minimized.

Copy link
Contributor

ivandardi commented Jan 30, 2019

@ivandardi what's about Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

Well, I'd assume that that is not possible, seeing that the Future trait only has a Output associated type.


@mehcode Well, it does address some of the concerns that were brought up previously, I'd say. It also helps decide on the prefix syntax, because there would be just one prefix await syntax instead of "Obvious Precedence" and "Useful Precedence" choices.

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 30, 2019

Well, I'd assume that that is not possible, seeing that the Future trait only has a Output associated type.

why not?

fn probably_get_future(val: u32) -> Result<impl Future<Item=i32, Error=u32>, &'static str> {
    match val {
        0 => Ok(ok(15)),
        1 => Ok(err(100500)),
        _ => Err("Coulnd't create a future"),
    }
}
@mehcode

This comment has been minimized.

Copy link
Contributor

mehcode commented Jan 30, 2019

@Pzixel See https://doc.rust-lang.org/std/future/trait.Future.html

You are talking about the old trait that was in the futures crate.

@I60R

This comment has been minimized.

Copy link

I60R commented Jan 30, 2019

Honestly, I don't think that having a keyword in prefix position how it was supposed to be:

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

has any strong advantage over having a sigil in the same position:

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

I think that it only looks inconsistently with other prefix operators, adds redundant whitespace before expression, and shifts code to the right side at noticeable distance.


We can try using sigil with extended dot syntax (Pre-RFC) which resolves issues with deeply nested scopes:

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

as well as adds possibility to chain methods:

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

And obviously, let substitute * with @ which makes more sense here:

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

What I like here is that @ reflects to await which is placed on LHS of function declaration, while ? reflects to Result<User> which is placed on the RHS of function declaration. This makes @ extremely consistent with ?.


Any thoughts on this?

@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 30, 2019

Any thoughts on this?

Any extra braces are just a no-go

@mehcode Yep, I didn't realize that futures now lack Error type. But the point is still valid: you can have a funtion, that probably return a feature, which, when completed, may return result or error.

@melodicstream

This comment has been minimized.

Copy link

melodicstream commented Jan 30, 2019

Any thoughts on this?

Yes! According to Centril's comment, sigils aren't very greppable. I'd only start to consider sigils if one can create a regex that identifies all the await points.

As for your extended dot syntax proposal, you'd have to explain it a lot deeper, providing the semantics and desugaring of every usage of it. As of currently, I can't understand the meaning of the code snippets you posted with that syntax.


But the point is still valid: you can have a funtion, that probably return a feature, which, when completed, may return result or error.

So you'd have a Future<Item=Result<T, E>>, right? Well, in that case, you just... await the future and deal with the result 😐

As in, suppose that foo is of type Future<Item=Result<T, E>>. Then await foo will be a Result<T, E> and you can use the following to deal with the errors:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())
@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 30, 2019

@melodicstream

So you'd have a Future<Item=Result<T, E>>, right? Well, in that case, you just... await the future and deal with the result 😐

No, I mean Result<Future<Item=Result<T, E>>, OtherE>

With postfix variant you just do

let bar = foo()? await?.bar();
@I60R

This comment has been minimized.

Copy link

I60R commented Jan 30, 2019

@melodicstream I updated my post and added link to pre-RFC for that feature

@huxi

This comment has been minimized.

Copy link

huxi commented Jan 30, 2019

Oh dear... I just re-read all the comments for the third time...

The only consistent feeling is my dislike for the .await postfix notation in all its variations because I'd seriously expect the await to be part of Future with that syntax. The "power of the dot" may work for an IDE but doesn't work for me at all. I could certainly adapt to it if it is the stabilized syntax but I doubt that it will ever really feel "right" to me.

I'd go with the super basic await prefix notation without any mandatory brackets, mainly because of the KISS principle and because doing it similar to most other languages is worth a lot.

De-sugaring await? future to (await future)? would be fine and appreciated but anything beyond that seems more and more like a solution without a problem to me. Simple let rebinding improved the readability of the code in most examples and I, personally, would likely go down that route while writing code even if easy chaining were an option.

With that being said, I'm quite happy that I'm not in a position to decide about this.

I'll now leave this issue alone instead of adding further noise and await (pun intended) the final verdict.

... at least I'll honestly try to do that...

@melodicstream

This comment has been minimized.

Copy link

melodicstream commented Jan 30, 2019

@Pzixel Assuming foo is of type Result<Future<Item=Result<T, E1>>, E2>, then await foo would be of type Result<Result<T, E1>, E2> and then you can just deal with that Result accordingly.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();
@Pzixel

This comment has been minimized.

Copy link

Pzixel commented Jan 31, 2019

@melodicstream no, it won't. You cannot await Result, you can awaitFuture. So you have to dofoo()?to unwrapFuturefromResult, then do awaitto get a result, then again?` to unwrap result from the future.

In postfix way it will be foo? await?, in prefix... I'm not sure.

So your examples just don't work, especially the last one, because it should be

(await foo.unwrap()).unwrap()

However, @huxi may be right we are solving the problem that probably doesn't exist. The best way to figure it out it allow postfix macro and see real codebase after basic async/await adoption.

@melodicstream

This comment has been minimized.

Copy link

melodicstream commented Jan 31, 2019

@Pzixel That's why I made the proposal to implement Future on all types of kind Result<Future<Item=T>, E>. Doing that would allow what I'm saying.

@earthengine

This comment has been minimized.

Copy link

earthengine commented Jan 31, 2019

Although I am OK with await foo?? for Result<Future<Output=Result<T, E1>>, E2>, I am NOT happy with await foo.unwrap().unwrap(). In my first brain model this have to be

(await foo.unwrap()).unwrap()

Otherwise I will be really confusing. The reason is that ? is a general operator, and unwrap is a method. The compiler can do something special for operators like the ., but if it is a normal method, I will assume it is always relates to the closest expression in its left hand side, only.

The postfix syntax, foo.unwrap() await.unwrap(), is also OK to me, as I know that await is just a keyword, not an object, so it have to be part of the expression before unwrap().

@Swoorup

This comment has been minimized.

Copy link

Swoorup commented Jan 31, 2019

postfix style macro solves much of these problems nicely, but just the question of do we want to retain familiarity with existing languages and keep it prefixed. I'd vote for postfix style.

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