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
Contributor

withoutboats commented Apr 6, 2018

Rendered

Companion libs RFC

Edit: Updated rendered link to merged location

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

This comment has been minimized.

Member

aturon commented Apr 6, 2018

@Manishearth

This comment was marked as resolved.

Member

Manishearth commented Apr 6, 2018

Can we also ask for async to be an edition-gated keyword in this RFC itself? That way we don't need to open a separate RFC for that, and ideally the keyword RFCs would be a part of their respective features.

We can also reserve await in case we plan to move to a proper syntax eventually.

@nikomatsakis

This comment was marked as outdated.

Contributor

nikomatsakis commented Apr 6, 2018

I'm curious to see an example where an async closure would be useful. I sort of expect async blocks to be the more common thing you want, but I'm probably overlooking something.

In particular, I'm wondering why give the sugar-y syntax to async closures, and not async blocks -- or both. Were there complications around async blocks that led to them being deferred?

(Obviously you can implement one in terms of the other, so it seems like the implementation work is more or less the same.)

@aturon

This comment was marked as outdated.

Member

aturon commented Apr 6, 2018

I'm curious to see an example where an async closure would be useful. I sort of expect async blocks to be the more common thing you want, but I'm probably overlooking something.

Put differently, when do you need a function producing a future rather than just a future, given that futures themselves are already "delayed" computations?

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.

@RalfJung

This comment has been minimized.

Member

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

This comment has been minimized.

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 was marked as outdated.

Member

cramertj commented Apr 6, 2018

@nikomatsakis

I'm curious to see an example where an async closure would be useful. I sort of expect async blocks to be the more common thing you want, but I'm probably overlooking something.

It's useful in the same sorts of positions that existing closures are useful-- higher order functions and things that take arguments. For example, imagine a function which takes in an async function as a way to handle HTTP requests:

fn main() {
    serve_http(async |request| {
        let body = await!(body)?;
        let response = await!(some_async_computation(body))?;
        Ok(response)
    })
}

However, I agree that async blocks are more generally useful. I think my personal preference leans slightly towards stabilizing async closures first, since they seem less controversial in design: async_block introduces a new block type which interacts with control-flow operators (e.g. return) in closure-like ways, which could be surprising to some. I'm also not totally sure about a syntax for move and non-move async blocks-- I'd imagined the external macros being async_block! { .. } and movable_async_block! { ... } or similar, but that seems to grotesque to introduce into the language natively.

@squishy-clouds

This comment has been minimized.

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 was marked as off-topic.

@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 was marked as off-topic.

@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 was marked as off-topic.

@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 was marked as off-topic.

@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

This comment has been minimized.

Contributor

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

This comment has been minimized.

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 was marked as outdated.

TyOverby commented Apr 6, 2018

For what its worth, the first time I read async_block! macro, my first thought was "oh, this takes some async code, and it blocks on it.

@vorner

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Member

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

This comment has been minimized.

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

This comment has been minimized.

Member

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

This comment has been minimized.

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

This comment has been minimized.

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 was marked as resolved.

comex commented Apr 7, 2018

Just a note that the syntax would have to be edition gated, since this compiles today:

    let async = true;
    let _ = async || { true };
@tmccombs

This comment was marked as resolved.

tmccombs commented Apr 7, 2018

If #[async] was used instead, no new keywords would be needed. At least for the first iteration.

@vorner

This comment was marked as outdated.

vorner commented Apr 7, 2018

@rpjohnst I would be against being await completely implicit. I find await much closer to try than to unsafe. While in unsafe, I need to be careful about everything (even outside of the block). With try, I want to see where exactly the function might exit, so I don't leave something in inconsistent state. It is a bit helped by RAII (eg. mutex is unlocked automatically), but I don't necessarily have to use RAII for all kinds of cleanups.

With await, even RAII don't help (because the mutex will stay locked while I await out of the function). Having an explicit mark of some kind greatly helps here.

Furthermore, 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 (eg. deadlocking from time to time).

Therefore, in line with Rusts aim to help programmer be confident in the code, I think it is worth having an explicit annotation of some kind (not necessarily a macro or macro-like keyword or an operator...).

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

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 was marked as resolved.

@Nemo157

Nemo157 Apr 7, 2018

Contributor

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

@MajorBreakfast

This comment has been minimized.

Contributor

MajorBreakfast commented May 5, 2018

After seeing the full expansion I think having a plan for some sort of sugar like this is necessary for ergonomic

@Nemo157 I agree

I suggest to specify this syntax in its own RFC and just list it as an unresolved question in this RFC. Can anyone post a nice description for this in a comment? I can't do it myself because there is too much magic involved 😅I'll then add it to my open PR.

@withoutboats

This comment has been minimized.

Contributor

withoutboats commented May 5, 2018

I think there's just a mental model mismatch going on between my post & @Nemo157's. From my perspective, there's no practical difference between returning an anonymous type that implements Future and returning impl Future, because there's no way to name the anonymous type except Fn::Output. I don't know any way that transitioning from -> impl Future to async fn is breaking, and functions with those signatures should be usable interchangeably. The differences are:

  • There's no need to introduce an existential variable that the compiler has to figure out is the anonymous type; this would just be wasted work.
  • Things like implementing Copy for the future when its environment is Copy are possible, making it return impl Future + Copy (similar to what we do today with closures). This isn't in the RFC, but its a future possibility.

As to traits, we'll have to "special case" (that is, have different rules) for impl trait in traits than bare impl traits. Either way, the situation with async fn is a direct parallel - we're going to be performing exactly the same transformation to an anonymous associated type whether we do it by "desugaring" to impl Future or not.

@eddyb

This comment has been minimized.

Member

eddyb commented May 5, 2018

@withoutboats Wait, this is the first time I see the suggestion of exposing the underlying type, as opposed to literally having impl Future.

There's no need to introduce an existential variable that the compiler has to figure out is the anonymous type; this would just be wasted work.

First off, this is really cheap. The body has to be type-checked anyway, deducing the impl Trait in the return type is peanuts compared to even a single method call.
Secondly, what's not cheap (although in a different sense) is adding yet another kind of type to the compiler - the only possible pre-existing choice is the generator state type and even that is unworkable, because it records results of inference (just like closure types).

If you "just" want to get the signature type from type inference results of the body, that's possible, but actually worse than -> impl Future because now you can't call it without type-checking it.
Aside from user limitations, this would potentially make incremental less effective for async fn.

Now, I could be totally off-base here, but it feels like there's an implementation plan here that hasn't been thoroughly discussed (with compiler stakeholders), and I'd like to see more of it.

EDIT: I hadn't provided a concrete example of an user-observable semantic difference, so here it is: recursive async fn (potentially mutually so): with -> impl Future you can box the future (to avoid infinite size) and cast it to a trait object (which may or may not be necessary in the future, depending on implementation details of impl Trait and potential support for structurally recursive types), but if the signature return type has to be extracted from the body, you can't do even that.
Here's a playground example demonstrating something similar with generators.

@Centril

This comment has been minimized.

Contributor

Centril commented May 5, 2018

@eddyb

@withoutboats Wait, this is the first time I see the suggestion of exposing the underlying type, as opposed to literally having impl Future.

I've assumed what boats said this entirely time; Interesting! :P

EDIT: The RFC specifies that the caller of an async fn can't assume OtherTrait even if the futures used in the async fn can.

What we could do is be initially conservative and literally have -> impl Future but possibly expose the actual underlying type (and Copy) later at our leisure since it is strictly speaking more allowing.

I think the actual underlying type should be exposed, because there is no way to say
-> impl Future + Copy from an async fn.

@MajorBreakfast

This comment has been minimized.

Contributor

MajorBreakfast commented May 7, 2018

The RFC specifies that the caller of an async fn can't assume OtherTrait even if the futures used in the async fn can.

@Centril What is OtherTrait? What section in the RFC? I can't quite follow

There's no need to introduce an existential variable that the compiler has to figure out is the anonymous type; this would just be wasted work.

@withoutboats Can you rewrite this sentence? It sounds like you changed what you want to type while typing it ^^'

@withoutboats

This comment has been minimized.

Contributor

withoutboats commented May 7, 2018

@MajorBreakfast sadly that is what I meant to type, even though its grammatical atrocious. But you should disregard it now.

I talked to @eddyb and in terms of implementation we're going to do what generators do, which is roughly the same as "-> impl Future". We agreed that the only important user facing aspect of this decision is recursion.

Its worth noting now that async functions cannot be called recursively without some kind of dynamic dispatch or at least heap allocation, because they would need to inline their state machine into themselves otherwise. The solution to this is for now is to cast the future into a trait object:

async fn foo() {
     await!(Box::new(foo()) as Box<Future<Output = ()>>)
}

In the long term, depending on how important this is, there are a couple of ways to make it better:

  1. TCO
  2. "dynamic call syntax" - some kind of syntax which virtualizes the call - await!(dyn foo()) for example.
@MajorBreakfast

This comment has been minimized.

Contributor

MajorBreakfast commented May 7, 2018

@withoutboats

  • Will the as Box<Future<Output = ()> after Box::new(foo()) be required?
  • Tail call optimization sounds like a very useful addition for the future
  • Dynamic call syntax: If the as Box<Future<Output = ()> in the example above is not required, then it's IMO nice enough. No special syntax required. Unless, I'm missing something. It won't avoid a heap allocation because that's unavoidable (except with TCO), right?
@eddyb

This comment has been minimized.

Member

eddyb commented May 7, 2018

@MajorBreakfast The Box indirection alone should be enough, the current implementation (of generators) produces a very strange error, and depending on the details, it might be fixable.

@withoutboats withoutboats merged commit a9d6699 into rust-lang:master May 8, 2018

@withoutboats

This comment has been minimized.

Contributor

withoutboats commented May 8, 2018

Per @aturon's comment on #2418, we are merging this RFC. 🎉 The tracking issue is rust-lang/rust#50547.

This does not commit us to a particular futures API. That is left as an unresolved question until #2418 or a successor is merged. For now, #2418 is left as an open RFC.

@withoutboats withoutboats referenced this pull request May 9, 2018

Open

Tracking issue for async/await (RFC 2394) #50547

1 of 10 tasks complete
@jjpe

This comment was marked as resolved.

jjpe commented May 9, 2018

Could somebody with write access update the rendered link to this? It is currently broken.

Edit: Done.

@alexreg

This comment has been minimized.

alexreg commented May 9, 2018

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

@joshtriplett

This comment has been minimized.

Member

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

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

This comment has been minimized.

Contributor

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

This comment has been minimized.

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 referenced this pull request Sep 2, 2018

Closed

Release 0.15 #476

@harrychin

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Member

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

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