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

async/await notation for ergonomic asynchronous IO #2394

Merged
merged 16 commits into from May 8, 2018

Conversation

@withoutboats
Copy link
Contributor

@withoutboats withoutboats commented Apr 6, 2018

Rendered

Companion libs RFC

Edit: Updated rendered link to merged location

boats

After gaining experience & user feedback with the futures-based ecosystem, we
discovered certain ergonomics challenges. Using state which needs to be shared
across await points was extremely ergonomic - requiring either Arcs or join

This comment has been minimized.

@sfackler

sfackler Apr 6, 2018
Member

*unergonomic

@aturon
Copy link
Member

@aturon aturon commented Apr 6, 2018

@Manishearth

This comment has been hidden.

@nikomatsakis

This comment has been hidden.

@aturon

This comment has been hidden.

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Apr 6, 2018

The code generated for await! contains unsafe. I'd like to see at least a one-sentence justification in the RFC why this code is correct. I believe it is because we create and control the local variable (future) that is being pinned here, and we know it's going to be dropped right after the loop and never going to be moved. This is essentially a variant of the macro-based stack pinning that AFAIK Servo uses. Is that right?


I think this is a good question, and it suggests an alternative design where we don't have async closures, but do have async blocks, which can of course be used with closures anyway.

One of the reasons to have the closures and not the blocks is that for the closures, nobody should find it surprising that they capture their environment and variables need to outlive the function body -- that's normal for functions returning closures. For a block, though, that behavior has no precedent.

@rpjohnst
Copy link

@rpjohnst rpjohnst commented Apr 6, 2018

Awesome work putting this together! I think this is a great spot to land in the design space.

It might be good, on some level, to frame async fn items as type definitions, with invocation as construction. This is similar to tuple structs, e.g. struct S(i32); and S(3), and it allows for a future expansion that lets users refer to the currently-anonymous types by name. This would more easily allow them to be placed in structs, allocated in groups, etc.

This framing also maps the concept of an async closure to something like a Java anonymous inner class. In that sense, an async block is actually the closer of the two concepts to existing sync closures- it's an expression that evaluates to a value (one that impls Future or Fn), rather than to a constructor. I also find the idea that an async block closes over its environment unsurprising, so I lean toward making async blocks the primitive.

@cramertj

This comment has been hidden.

@squishy-clouds
Copy link

@squishy-clouds squishy-clouds commented Apr 6, 2018

What is the reasoning behind using an async keyword, as opposed to something like #[async]?

A final - and extreme - alternative would be to abandon futures and async/await
as the mechanism for async/await in Rust and to adopt a different paradigm.
Among those suggested are a generalized effects system, monads & do notation,

This comment has been minimized.

@Centril

Centril Apr 6, 2018
Contributor

First I want to say that I think this really well written (and readable) RFC. 🥇
I think it also lands very nicely.


In particular, my initial thinking here, is this RFC is forward-compatible with forms of effect polymorphism; which is quite nice.

One could imagine ?async as "possibly async", i.e: the callee decides on sync or async.
And ?const as "possibly const", -- again, the callee decides.
Then we can mix these and you get 4 different modes.
How useful all of this is an open question.

Going a bit further (or too far) effects may be try async which could be the same as the Result-embedded futures encoding?


Also, should async fn(T) -> R as a function type be a thing?

This comment has been minimized.

@RalfJung

RalfJung Apr 6, 2018
Member

Also, should async fn(T) -> R as a function type a thing?

Isn't that just fn(T) -> impl Future<Item=R>?

This comment has been minimized.

@Centril

Centril Apr 6, 2018
Contributor

That seemed to be the consensus on #rust-lang, yes =)

The question was more: should the surface syntax be a thing?
with analogy with const fn(T) -> R.

This comment has been minimized.

@Centril

Centril Apr 6, 2018
Contributor

Some further notes on the effect polymorphism angle (still noting that this is just "hypothetical"):

We could possibly interpret async fn foo(x: T) -> R {..} as the following:
effect<E> (E + async) fn foo(x: T) -> R {..}.
Which universally quantifies the ambient effect E and then you combine E + async.

But the body of foo can't assume anything about what E might be, so this should also be backwards compatible in the future with such as change?

This comment has been minimized.

@RalfJung

RalfJung Apr 6, 2018
Member

The question was more: should the surface syntax be a thing?
by analogy with const fn(T) -> R.

Well, very much unlike const, async can be just syntactic sugar.

@Nemo157
Copy link
Member

@Nemo157 Nemo157 commented Apr 6, 2018

I really love how this turned out (slightly disappointed that Streams are not included in the first cut, but not surprised, seems like by the time this is stabilised enough of the pieces should be in place to allow a not too bad library solution for Streams anyway).

Surprisingly after how pro-"actual return type" I was I don't really mind how this looks. I think a big part of my revulsion with the current implementation is that the specific transform Result<T, E> -> Future<Item = T, Error = E> seems far too special with pulling the input type apart to construct the output type; whereas the proposed T -> Future<Item = T> transform is such a straight-forward thing that it doesn't have the same effect.

Despite liking the proposed return type treatment I still feel like the lack of being able to use named existential types is a downside; it removes the ability to further constrain a Future returning trait based on things like Send while using async in the implementation (you could instead use async_block, but that's slightly less ergonomic):

trait Client {
    type Request<'a>: Future<Item = String> + 'a;
    fn get(&mut self) -> Self::Request<'_>;
}

fn foo<C>(client: C) where C: Client, C::Request: Send {
     ...
}
@rpjohnst
Copy link

@rpjohnst rpjohnst commented Apr 6, 2018

@Nemo157 The framing I described above, treating async fns as types rather than functions, should help that situation.

@TyOverby

This comment has been hidden.

@vorner
Copy link

@vorner vorner commented Apr 6, 2018

I'm not sure I personally prefer yet another keyword that can be put in front of fn, over an attribute, but I guess opinions on that differ.

Anyway, while it is a keyword, would it make sense to explicitly state its position in relation to other such keywords? For example, pub unsafe fn is valid, but unsafe pub fn is not. Where does async fit in here?

@rpjohnst
Copy link

@rpjohnst rpjohnst commented Apr 6, 2018

One bikeshed alongside the idea of async fns-as-types: might it be worth changing the construction syntax from foo() to something more explicit, at least outside of await!(foo())? This syntax would only be used when you don't want to immediately await the future- e.g. when transitioning from a sync context, or when setting up to use the join combinator.

This would make it more clear that you're not running any of the function body until you actually await it, which helps with /u/munificent's comment about the Dart team's experience here:

we see nasty concurrency bugs where people think they can do some synchronous work at the top of a function and are dismayed to discover they've created race conditions. Overall, it seems users do not naturally assume an async function yields before executing any code.

Making call syntax available in await!(foo()), but not elsewhere, also makes it clear that you can't just call an async fn from a sync context and expect something to happen (which partially overlaps with the above confusion, but also with #[must_use]).

In fact, async { } is sufficient on its own and could be that more-explicit syntax:

tokio::run(async {
    let a = async {
        ... await!(foo(1, 2, 3)) ...
    };
    let b = async {
        ... await!(bar("a", "b", "c")) ...
    };
    await!(join(a, b)) 
});

This also has two future-proofing effects:

  • If constructing an async-based future is only possible under controlled circumstances (as part of await!, or as an async { } block) then we remain future-compatible with a CPS transform.
  • If the bare function call syntax is reserved, it could become what await! expands to, leaving that option open. (For reference, this is how Kotlin works, and it's a lot less noisy than using an await keyword.)

And an even smaller bikeshed: the list of things that await! might eventually be replaced with could also include a postfix operator (e.g. foo() await ?, foo() @ ?). This would be precisely the same trajectory that try! took to become ?, which means it would get the same benefits- chaining becomes easier. (Of course the bare function call syntax, i.e. "implicit await", also addresses the same issue.)

@cramertj
Copy link
Member

@cramertj cramertj commented Apr 6, 2018

@rpjohnst IMO the ergonomic difference between

tokio::run(async {
    let a = async {
        ... await!(foo(1, 2, 3)) ...
    };
    let b = async {
        ... await!(bar("a", "b", "c")) ...
    };
    await!(join!(a, b)) 
});

and

tokio::run(async {
    await!(join!(foo(1, 2, 3), bar("a", "b", "c"))) 
});

isn't worth whatever clarity benefits you gain from "forcing" async fn to be awaited immediately. Also, I think that in most cases, it's an implementation detail of the async fn whether or not it has any immediate behaviors, so this distinction isn't important from the caller's perspective (e.g. /u/munificent's example on Reddit was using it for caching).

@rpjohnst
Copy link

@rpjohnst rpjohnst commented Apr 6, 2018

Erm, I intended a and b to be large blocks in that example, not something you could reasonably inline.

But you're right, it is a bit worse in that sense. On the other hand, it leaves open the option of implicit await, so it might look like this instead:

tokio::run(async {
    join(async { foo(1, 2, 3) }, async { bar("a", "b", "c") })
});

Basically, annotate the uncommon case of not immediately awaiting, and leave the common case of immediate awaiting noise-free.

@cramertj
Copy link
Member

@cramertj cramertj commented Apr 6, 2018

@rpjohnst

annotate the uncommon case of not immediately awaiting, and leave the common case of immediate awaiting noise-free

That's a cool idea, but again it creates a distinction between async fn and fn() -> impl Future that I don't think we want. That is, I think it would be confusing to special-case -> impl Future functions to be auto-awaited, but only auto-awaiting async fn would make async fn interestingly distinct from fn() -> impl Future.

Explicit await is also useful for discerning which parts of your program might yield to an event loop.

@egilburg
Copy link

@egilburg egilburg commented Apr 7, 2018

If async is going to become a keyword (and not just an attribute), should await be a (non-macro) keyword as well?

@rpjohnst
Copy link

@rpjohnst rpjohnst commented Apr 7, 2018

@cramertj Ah, that's a good point. Depending on how common we expect -> impl Future functions to be relative to async fns, it may not be worth the confusion.

On the other hand, I'm not sure that's any different from just calling a sync function. There's always the possibility that you'll need to look up a function signature, and the fact that async fns' return types are different from -> impl Future functions' will help.

So while we don't need to decide the particular await syntax yet, it would still be nice to be forward compatible with implicit await (i.e. not directly expose "foo() has type impl Future"). Here's a summary of the points I've seen/made in its favor in past discussions:

  • It neatly solves the interaction with ?- there's no precedence confusion, no problems with chaining, no sigils.
  • It's similar to unsafe in annotation density- you have to be in an async function or block, but then you can await things without further annotation. That may be enough information about where you might yield to an event loop.
  • It's much less noisy. Smaller combinator-like async functions are nicer to use. Kotlin went this route and simply has an await method on its futures for when you aren't calling and awaiting at the same time.
  • It leaves the door open for eventually making combinators like unwrap_or_else async-polymorphic, where the passed-in closure might want to be async. Doing async polymorphism with explicit await means you have to figure out a syntax for "await this if it's async, otherwise don't."
@comex

This comment has been hidden.

@tmccombs

This comment has been hidden.

@vorner

This comment has been hidden.

3. Define the precedence as the inconvient precedence - this seems equally
surprising as the other precedence.
4. Introduce a special syntax to handle the multiple applications, such as
`await? future` - this seems very unusual in its own way.

This comment has been minimized.

@scottmcm

scottmcm Apr 7, 2018
Member

  1. make await a postfix operator instead.

Every time I need (await Foo()).Bar in C# I'm sad and wish it were something postfix.

This comment has been minimized.

@est31

est31 Apr 7, 2018
Contributor

What about Bar(Foo() await) ? :p

This comment has been minimized.

@Centril

Centril Apr 7, 2018
Contributor

Could you elaborate on why it should be postfix?

I think await Foo() reads better for an English (and at least Scandinavian) reader since you are (a)waiting for Foo() to complete.

This comment has been minimized.

@rpjohnst

rpjohnst Apr 7, 2018

It solves the precedence confusion around ? and makes chaining easier. Basically the same arguments for going from try! to ?.

This comment has been minimized.

@scottmcm

scottmcm Apr 7, 2018
Member

@Centril I agree it looks better for simple cases, but once you have multiple or need to mix it with methods and fields, it gets worse. I think x.foo().await.bar().await.qaz (with highlighting) will look way better than (await (await x.foo()).bar()).qaz -- fewer hard-to-track-down parens and maintains the reading order.

(Prefix would probably be better in Lisp or Haskell, but postfix seems generally better in rust. I sometimes even wish we had no prefix operators, so there'd never be a !(foo?) vs (!foo)? confusion...)

Edit: to show the difference keyword highlighting would make for emphasizing await points:

x.foo().let.bar().qaz.yay().let.blah

This comment has been minimized.

@vi

vi Apr 8, 2018

In any case future await ? should be added to the list as 5., without much analyzing if it is good or bad.

This comment has been minimized.

@vi

vi Apr 8, 2018

Like with the ? instead of try!, it puts the actual thing (what we are calling?) at the first place, and boilerplate-y technicalities (yield point or regular call?) at the end.

This comment has been minimized.

@tmccombs

tmccombs Apr 8, 2018

I like the idea of a postfix await operator, but I'm not sure what the best syntax for that would be.
f.await looks too much like accessing a field f.await() looks too much like a method call f await is just as awkward to use as await f.

This comment has been minimized.

@vi

vi Apr 9, 2018

Or it can be just some another special character, like future@? or future~?.

@HadrienG2
Copy link

@HadrienG2 HadrienG2 commented Apr 7, 2018

@rpjohnst Would your concern about the syntax similarity between async and non-async fns leading to dropped futures be addressed by marking futures and streams as #[must_use], as is being discussed in the companion RFC?

@glaebhoerl
Copy link
Contributor

@glaebhoerl glaebhoerl commented Apr 7, 2018

SGTM. I vaguely prefer @rpjohnst's implicit-await-explicit-async formulation. I keep hearing positive vibes about Kotlin's coroutines, and maybe we should have some of what they're having.

if a dependency makes a breaking change and makes one of its functions from blocking to future and implicit awaiting is in place, it'll still compile, but introduce a hard to find subtle bug

Calling a blocking function in async code would have already been a bug, wouldn't it?

loop {
match Future::poll(Pin::borrow(&mut pin), &mut ctx) {
Async::Ready(item) => break item,
Async::Pending => yield,

This comment has been hidden.

@Nemo157

Nemo157 Apr 7, 2018
Member

As @glaebhoerl mentioned on the companion RFC the Async alias is not in the proposed API, these should be Poll::.

@jjpe

This comment has been hidden.

@alexreg
Copy link

@alexreg alexreg commented May 9, 2018

@withoutboats @Centril Does async work as an actual "effect" here?

@joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented May 16, 2018

Cross-referencing: #2442 would allow a postfix await on an expression (expr.await!()), including in a left-to-right chain (future().await!().more_computation().await!()).

@warlord500
Copy link

@warlord500 warlord500 commented Jun 6, 2018

I am curious what is the difference between async Closure and async block?
is the reason for async closures to keep constinency with async fn's?

@MajorBreakfast
Copy link
Contributor

@MajorBreakfast MajorBreakfast commented Jun 6, 2018

@warlord500 Async closures are like async functions in the regard that you need to call them to get a future. So, consistency is a reason. An async block is an expression that evaluates immediately to a future. We're also not sure whether they should allow using the return statement. Async blocks will probably be more common than async closures. But, to cite the RFC "it seems inevitable that we will want both of them".

@Nemo157
Copy link
Member

@Nemo157 Nemo157 commented Jun 6, 2018

For an example of where you would want to use each, async blocks are useful for having some synchronous immediately executed code when constructing a future:

fn get(url: impl Into<Url>) -> impl Future<Output = Response> {
    let url = url.into();
    async {
        // do the request
    }
}

while I mostly see async closures as useful for things like stream adaptors (this is including a few hypothetical apis, not sure how they'll really look)

async fn sum_counts(urls: impl Iterator<impl Into<Url>>) -> Result<u32, Error> {
    let counts = await!(stream::iter(urls).parallel(4).map(async |url| {
        let res = await!(get(url))?;
        let value: SomeStruct = await!(serde_json::from_async_reader(res.body()))?;
        Ok(value.count)
    }).collect::<Result<Vec<u32>>>())?;
    Ok(counts.sum())
}
@Ekleog
Copy link

@Ekleog Ekleog commented Aug 9, 2018

Some musing about the possibility of tail-call optimization with async fn. (tl;dr: nothing to change)

From what I guess, the compiler will not even try tail-call optimizing an async fn, because it would be impossible to do it in a 0-cost way. As a consequence, I wonder whether some support may be needed in the task system for this.

The only scheme I can think of would be to wrap the result of the async fn in a custom Future-implementing type, that does dynamic (enum- or trait-object-based) dispatch over an internal data structure, in order to figure out which future to poll. Then it polls it, and the future completes with either its final return, or a future to chain the result to. If it returns a future to chain, then said future is wrapped inside the enum or trait object of the wrapping future, and is then polled.

For trait-object-based handling, I think it's already possible to do this exclusively with a third-party crate, that would just have to provide the said wrapping future type.

For enum-based handling, I more or less think it cannot possibly be done while remaining sane. It would first require the compiler to provide in some way the closure of all future types that can be called as tail-calls from a future, and then a crate to actually wrap it in a nice wrapping-future type. Overall, I think the potential gain (enum vs trait object dispatch) is not worth the added complexity.

@dtolnay dtolnay mentioned this pull request Sep 2, 2018
@harrychin
Copy link

@harrychin harrychin commented Oct 10, 2018

@Ekleog Just as some additional context, there was a suggestion from @rpjohnst to have a CPS-style transformation added to generators/futures which would allow futures to drive themselves which would lend itself well to TCO.

https://internals.rust-lang.org/t/pre-rfc-cps-transform-for-generators/7120/23

It's hard to say whether that scheme would be "zero-cost", as the generators would be storing the pointers on the stack, but it would save cycles from having the CPU do extra polling and assist TCO.

@oleganza
Copy link

@oleganza oleganza commented Nov 13, 2018

Re: syntax for await future? where we need to ?-unwrap the Result returned from the Future.

Has anyone considered using @ in place of await? This way we can do @foo.bar()? and it would be equivalent to await!(foo.bar())?.

I personally prefer spelled-out keywords (await), and doing await? foo.bar looks like a reasonable option to me.

@eddyb
Copy link
Member

@eddyb eddyb commented Nov 13, 2018

@oleganza Yes, @ has been proposed. I remember calling it something like "the «spin until it's done and gives a value back» operator" (if you think of @ as a spiral).

hcpl added a commit to hcpl/rust.vim that referenced this pull request Nov 24, 2018
* `async` from rust-lang/rfcs#2394;
* `existential` from rust-lang/rfcs#2071.
da-x added a commit to rust-lang/rust.vim that referenced this pull request Nov 29, 2018
* Add new keywords

* `async` from rust-lang/rfcs#2394;
* `existential` from rust-lang/rfcs#2071.

* Make `existential` a contextual keyword

Thanks @dlrobertson who let me use his PR
#284!
@withoutboats withoutboats mentioned this pull request Jun 26, 2019
2 of 4 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment