# Async Closures
This post attempts to motivate some of the concrete technical reasons why the `#![feature(async_closure)]` that I've been working on recently[^rework][^more][^more2][^more3] is so complicated. Specifically, why can't we just use `|| async {}` that we can express on stable today, an interesting but very important note about the relationship between a "lending" `FnMut` and the `FnOnce` trait that I haven't seen written anywhere yet, and how async closures are a tractable solution to an otherwise intractable problem with lending closures.
[^rework]: https://github.com/rust-lang/rust/pull/120361
[^more]: https://github.com/rust-lang/rust/pull/120712
[^more2]: https://github.com/rust-lang/rust/pull/123518
[^more3]: https://github.com/rust-lang/rust/pull/125259
### We have async closures at home...
You may have tried in the past to make your closures async by writing a regular closure that returns an `async` block:
```rust!
let x = || async {};
```
This is indeed a closure that returns a future. However, let's try actually using it practically.
Imagine having some service framework, and a query that you can use to do something for each of its registered services:
```rust!
/// Queries a database for all services, allowing you
// to run a callback for each service definition.
async fn query_all_services(
x: impl FnMut(ServiceDefinition) -> impl Future<Output = ()>,
) { /* impl */ }
```
side-note ... that's [not valid Rust](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=270c2e9ad2cd266925351bc1278b1ace)[^itit]. So, let's try that again:
[^itit]: The exact meaning of an `impl Fn() -> impl Trait` is a bit up in the air... so we've hesitated to give it an exact meaning just yet.
```rust!
async fn query_all_services<F, Fut>(x: F)
where
F: FnMut(ServiceDefinition) -> Fut,
Fut: Future<Output = ()>,
{ /* impl */ }
```
And now let's try to use this pseudo-async closure.
For example, let's gather up all of the service IDs for every service, so we can list them. Imagine we have some `async fn ServiceDefinition::id(&self) -> ServiceId` accessor[^1], and call it in our closure:
[^1]: You can come up with a reason why the accessor method is `async` -- maybe it needs to do a lookup in some some global service database or something.
```rust!
let mut service_ids = vec![];
query_all_services(|svc| async {
service_ids.push(svc.id().await);
});
```
Uh...
```
error: captured variable cannot escape `FnMut` closure body
--> src/main.rs:22:30
|
21 | let mut service_ids = vec![];
| --------------- variable defined here
22 | query_all_services(|svc| async {
| ____________________________-_^
| | |
| | inferred to be a `FnMut` closure
23 | | service_ids.push(svc.id().await);
| | ----------- variable captured here
24 | | });
| |_____^ returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
```
Let's read that error message carefully:
> returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
Why are we returning a reference to a captured variable? Oh, yeah -- we capture a mutable reference to `service_ids` in order to push the IDs onto it.
```rust!
let mut service_ids = vec![];
query_all_services(|svc| async {
service_ids.push(svc.id().await);
// ^^^^^^^^^^^ We capture `&mut Vec<ServiceId>` here.
});
```
In order call the callback multiple times, the closure _reborrows_ the mutable reference it captured on every invocation, and it hands back a future which borrows that mutable reference from the closure itself.
So what gives? Is this really something we can't fix?
### Async closures are (kinda) lending closures
This limitation is related to an interesting blog post that Niko wrote a while back: ["Giving, lending, and async closures"](https://smallcultfollowing.com/babysteps/blog/2023/05/09/giving-lending-and-async-closures/)[^2].
[^2]: You should read the section "**Async closures are a lending pattern**".
Specifically, `FnMut` is not currently a *lending* trait, since its output type does not have a lifetime that allows us to link the `&mut self` of the call with the output.
But what if it were? Let's try to adapt the `LendingFnMut` trait that Niko laid out.
But first, let's do a bit of due diligence -- keep in mind the trait hierarchy that we are accustomed to in Rust. Specifically, `FnMut` is a subtrait of `FnOnce`, because if we can call a closure multiple times we should definitely be able to call it once. So adapting that to our `LendingFnMut` trait:
```rust
trait LendingFnMut<A>: FnOnce<A> { // `FnOnce` supertrait.
fn call_mut<'s>(&mut self, args: A) -> Self::Output<'s>;
}
trait FnOnce<A> {
type Output<'this>
where
Self: 'this;
fn call_once<'s>(self, args: A) -> Self::Output<'s>;
}
```
Well that's odd... what's up with that `'s` lifetime on `call_once` future? It's not present anywhere in the inputs -- it's just kinda dangling there in the output.
Is there anything else we could put there instead? If we put `'static` there, then we'd have a really restrictive `type Output`, since we would then need to satisfy `Self: 'static`... How strange. Well, let's keep it for now. It definitely won't come back to bite us later :thinking_face:
Ok, let's try these definitions out. First, let's express our `query_all_services` function with `LendingFnMut`:
```rust
async fn query_all_services<F, Fut>(x: F)
where
F: for<'s> LendingFnMut<ServiceDefinition, Output<'s>: Future<Output = ()>>,
{ /* impl */ }
```
OK, kind of a hairy definition, but nothing we can't take apart piece-by-piece.
So what it's saying is that `F` is a lending closure whose output type is a `Future` that returns `()` when awaited. It's using the [associated type bound](https://rust-lang.github.io/rfcs/2289-associated-type-bounds.html) syntax that I [recently stabilized](https://github.com/rust-lang/rust/pull/122055), and it's saying that the output is some `Future` type that `await`s to `()`, which may be generic over the `'s` parameter -- that's the lending part.
So yeah, now let's write the caller code. Annoyingly we need to write a manual implementation of this trait instead of using a closure... let's try doing that, using the hopefully-soon-to-be-stabilized `#[feature(impl_trait_in_assoc_type)]`:
```rust
let mut service_ids = vec![];
query_all_services(Callback { service_ids: &mut service_ids });
struct Callback<'a> {
service_ids: &'a mut Vec<ServiceId>,
}
impl<'a> FnOnce<ServiceDefinition> for Callback<'a> {
type Output<'this> = impl Future<Output = ()>
where
Self: 'this;
fn call_once<'s>(self, svc: ServiceDefinition) -> Self::Output<'s> {
async move {
self.service_ids.push(svc.id().await);
}
}
}
```
Cool, we've got the `FnOnce` implementation down. What about the `LendingFnMutOnce` implementation...
```rust!
impl<'a> LendingFnMut<ServiceDefinition> for Callback<'a> {
fn call_mut(&mut self, svc: ServiceDefinition) -> Self::Output<'_> {
async move {
self.service_ids.push(svc.id().await);
}
}
}
```
```
error[E0308]: mismatched types
--> src/lib.rs:46:9
|
33 | type Output<'this> = impl Future<Output = ()>
| ------------------------ the expected future
...
45 | fn call_mut(&mut self, svc: ServiceDefinition) -> Self::Output<'_> {
| ---------------- expected `<Callback<'a> as FnOnce<ServiceDefinition>>::Output<'_>` because of return type
46 | / async move {
47 | | self.service_ids.push(svc.id().await);
48 | | }
| |_________^ expected future, found `async` block
|
= note: expected opaque type `<Callback<'a> as FnOnce<ServiceDefinition>>::Output<'_>`
found `async` block `{async block@src/lib.rs:46:9: 48:10}`
```
Oh, that's awkward. Every async closure is indeed a separate type -- what do we do? Let's try to reuse one for the other by defining the `FnMut` implementation first, and then dispatching the `FnOnce` implementation to that, like regular closures do[^fnonce]:
[^fnonce]: the `FnOnce` implementation for an `FnMut` closure will just dispatch to the `FnMut` impl. It's called the "`FnOnce` shim" in `rustc` parlance.
[[code] omitted because it's a disaster...]
Well... that also doesn't work. I won't go into major details, but here's [the code][code] if you're curious. I'll summarize the gist of the issue in just a second. First, let's think about this more abstractly.
[code]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=f2802b2c34f1a350f917269f6a3a649e
### Two different output types
So, it turns out that we made a mistake when laying out the definition of the `LendingFnMut`/`FnOnce` trait hierarchy. Specifically, we need *two* different output types:
```rust!
trait LendingFnMut<A>: FnOnce<A> {
// Let's call the lending flavor of this output `LendingOutput`.
type LendingOutput<'this>
where
Self: 'this;
fn call_mut<'s>(&'s mut self, args: A) -> Self::LendingOutput<'s>;
}
trait FnOnce<A> {
// Whereas the output of `FnOnce` is -- this makes sense, there's
// really no borrowing happening here, so it makes sense that this
// output doesn't need to be parameterized over a lifetime.
type Output;
fn call_once(self, args: A) -> Self::Output;
// That solves out mysterious lifetime question from earlier, too.
}
```
OK -- and if we try to employ this:
```rust!
impl<'a> FnOnce<ServiceDefinition> for Callback<'a> {
type Output = impl Future<Output = ()>;
fn call_once(self, svc: ServiceDefinition) -> Self::Output {
async move {
self.service_ids.push(svc.id().await);
}
}
}
impl<'a> LendingFnMut<ServiceDefinition> for Callback<'a> {
type LendingOutput<'s> = impl Future<Output = ()>
where
Self: 's;
fn call_mut(&mut self, svc: ServiceDefinition) -> Self::LendingOutput<'_> {
async move {
self.service_ids.push(svc.id().await);
}
}
}
```
Hey! It works! Check for yourself: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=358bad2b87783e690163886f329c7e37
So what's going on here? We literally just copied the body from `call_once` to `call_mut`, right?
Wait -- let's read between the lines in the implementations of both functions. While both of them share the same set of syntax tokens, each actually ends up capturing a different type! Specifically, the `call_once` future ends up capturing `Self` (i.e. `Callback<'a>`), and the `call_mut` future ends up capturing `&mut Self` (i.e. `&mut Callback<'a>`).
That's interesting, and actually the root cause of the issue in [the code I linked above][code]. Specifically, there's no way to share the implementation between `LendingFnMut` and `FnOnce` because generally return different futures!
OK, so stepping back out of the world of manual trait implementations, for this to work for *real* closures, we just need to teach `rustc` how to generate this new definition of `LendingFnMut`, with its own distinct return type, whenever it gets into a situation where it detects a borrow coming from the closure itself, right? Will that work in general?
### Lending closures are not typically `FnOnce`
Let's take a second to step and think about a general lending closure. For example, imagine a closure that lends out a reference to a string that it captures by value:
```rust!
let string = String::from("hello, world!");
let view_string: /* impl Fn() -> &str */ = move || &string;
```
Now, since all closures implement `FnOnce`, what would the `FnOnce` implementation for that closure even *return*? Reminder that the `FnOnce` signature is:
```rust!
fn call_once(self, args: Args) -> Self::Output;
```
Notably, we *consume* the `self` argument in the process of calling the closure. So if we've moved the `String` into the closure, we consequently drop it.
It turns out that this is intimately related to the problem we hit above, where we have separate bodies for the `LendingFnMut` and `FnOnce` implementation. Unlike the async case above, we can't just copy the body:
```rust!
impl FnOnce<()> for /* {the `view_string` closure} */ {
type Output = /* what do we even put here!? */;
fn call_once(self) -> Self::Output {
&self.string
// Also, oh no! We have an escaping reference.
}
}
```
So.... what do we do? Well, I guess we could specify a totally separate body for the `FnOnce` impl for the lending closure -- for example, we could make it so that the closure returns the captured string *by value* when it's called with `FnOnce::call_once`...
... but, I don't believe this process generalizes in a useful way. I could be the devil's advocate, and ask "what if"s about so many different types that would require their own strategy to fix. For example, you could imagine lending closures that return arbitrarily complex types, like [`Ref<'_, T>`](https://doc.rust-lang.org/std/cell/struct.Ref.html), [`repr(transparent)`](https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent)-transmuted references, etc.
This is a very important detail that I don't think was discussed in Niko's lending blog post (maybe it was, but I didn't see it :sweat_smile:) -- however, it is a very important detail since it affects the way we design async closures.
### All async closures admit a `FnOnce` implementation
So given that problem above, what makes async closures *doubly* interesting is that it really doesn't suffer it at all, actually.
Specifically, I'm claiming that they really do admit an obvious `FnOnce` implementation even if you only ever provide the `LendingFnMut` implementation. We can always create a meaningful `FnOnce` implementation by taking the `Future` returned by the async closure and making all of the types that were captured by-ref into ones captured by-move, and adjusting some borrows to make up for that.
That's basically what we did when we provided the `FnOnce` implementation above -- it just came for free to us because we captured `self` in the resulting `Future` rather than `&mut self`. In reality, it's a bit more complicated when you factor in the `move` keyword, the 2021-closure-capture rules laid out in [RFC 2229](https://rust-lang.github.io/rfcs/2229-capture-disjoint-fields.html).
I was going to go into more detail exactly how this transformation worked, since there are a lot of fun caveats having to do with captures modes and mutability and the `FnOnce` trait and stuff, but I didn't have enough space to write in the margins (/jk) and in reality I'm just a bit too lazy to write it all up, since it's really technical. If you want to see the nasty gory details, check out the `by_move_body` pass that I implemented, which transforms the body of a by-ref future into a by-move one. This is what takes the returned future of `LendingFnMut`[^async_fn_mut] and turns it into a future that is compatible with `FnOnce`: [`by_move_body.rs` in the compiler](https://github.com/rust-lang/rust/blob/342c1b03d6d0f9aa1e8119a2556d819af024faaf/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs). You may also want to read [here](https://github.com/rust-lang/rust/blob/342c1b03d6d0f9aa1e8119a2556d819af024faaf/compiler/rustc_hir_typeck/src/upvar.rs#L213-L259) and [here](https://github.com/rust-lang/rust/blob/342c1b03d6d0f9aa1e8119a2556d819af024faaf/compiler/rustc_hir_typeck/src/upvar.rs#L1805-L1847) in the `upvar.rs` capture analysis code in the type checker.
[^async_fn_mut]: Well, in reality it's [`AsyncFnMut`](https://doc.rust-lang.org/1.79.0/std/ops/trait.AsyncFnMut.html). More about that directly below.
### An `AsyncFnOnce` hierarchy:
Somewhat unrelated to the problems above (but useful for some summarizing discussion below), it's actually most useful to abandon `LendingFn*` for now and define a set of `AsyncFn*` traits that are specialized to async closures. I proposed this in [`PR #119305`](https://github.com/rust-lang/rust/pull/119305). Specifically, we have:
```rust!
trait AsyncFnOnce<A> {
type CallOnceFuture: Future<Output = Self::Output>;
/// Value returned from awaiting the future.
type Output;
fn call_once(self, arg: A) -> Self::CallOnceFuture;
}
trait AsyncFnMut<A> {
type CallMutFuture<'s>: Future<Output = Self::Output>
where
Self: 's;
// Notably, this is a different future type, but `Self::Output`
// is the same.
fn call_mut(&mut self, arg: A) -> Self::CallMutFuture<'_>;
}
/// `AsyncFn` left out since it's practically the same as `AsyncFnMut`.
```
These traits will be perma-unstable just like the `Fn` traits, and they will only be nameable through the "paren sugar" trait bounds that users are used to.
Specifically, users can just write `F: AsyncFnMut() -> i32`. We will likely simplify this with an `async` trait bound modifier rather than naming `AsyncFnMut` directly (e.g. `F: async FnMut() -> i32`), but we'll keep the trait perma-unstable to name directly. This means that we may swap out this trait for a general `LendingFnMut` trait (and make `AsyncFnMut` into a trait alias of sorts).
This set of traits also simplifies some other problems that come with a general `LendingFnMut` trait, that I don't have time to go into in this post, specifically:
* It makes async closure *signature inference* possible.
* It avoids some higher-ranked trait bound issues that would be present with `LendingFnMut`.
* It allows us to tailor error messages for when this trait is unimplemented.
### Final thoughts
Why does this work for async closures, but `LendingFnMut` doesn't have an intuitive solution in general?
Well, async closures can be seen as a specialization of lending closures, specifically limiting the return type in a way that makes tractable to problem of how to generalize closure's body into a meaningful `FnOnce` implementation.
Specifically, while the user has the power to specify whatever `Output` type that they want, they still can't write futures whose output captures from the closure, that is:
```rust
// Not allowed!
let string = String::from("hello, world!");
let view_string: = async move || -> &str { &string };
let fut = AsyncFnOnce::call_once(view_string, ())
// What does this even return? `view_string` was consumed by
// the call above, and the future was consumed by awaiting it.
// therefore `string` is no longer with is -- we can't take
// a reference to data that longer exists.
let _: /* &'? str */ = fut.await;
```
This is particularly important, since without this limitation, we'd be back at square zero, the general lending closure problem. This is enforced, specifically, by the fact that while the `AsyncFnMut::CallMutFuture` associated type has a GAT lifetime:
```rust
type CallMutFuture<'s>: Future<Output = Self::Output>
where
Self: 's;
```
The `AsyncFnOnce::Output` type, which represents the value returned by awaiting the future does not:
```rust
type Output;
```
And both the `AsyncFnOnce` and `AsyncFnMut` implementation share this output type, even if they have different future types.
### tl;dr
* Async closures need to be *lending*, since they return futures that may borrow from the closure's captures.
* So they need to implement some `LendingFnMut` trait.
* However, `LendingFnMut` is not easy to introduce into the current `Fn->FnMut->FnOnce` trait hierarchy, because `LendingFnMut` cannot be a subtrait of `FnOnce`. Why? Because:
* The value returned by a `LendingFnMut` implementation typically differs from that returned by `FnOnce`, but they're inherited by the current subtraits.
* So let's split out these two types -- however, in general, there's no meaningful value for a lending closure to return when called in a way that consumes the closure.
* That is: lending closures typically do not implement `FnOnce` at all.
* So we can't just fix `FnMut` and make it "just work".
* However, Async closures *do* have an obvious `FnOnce` implementation that can be derived programmatically from their `LendingFnMut` implementation.
* To simplify closure signature inference, it's easiest to stop using the most general definition (`LendingFnMut`) and specialize this trait for async -- we introduce `AsyncFn`/`AsyncFnMut`/`AsyncFnOnce`.
* But to keep space for future generalization, we can hide this behind an `async` trait bound modifier.
That leads us to the current design of async closures, which I believe are quite ergonomic and powerful. I recommend you try them out next time you're on nightly -- just write `async` before your `Fn()` trait bounds and use `async ||` instead of `|| async` and hopefully your errors will just disappear :crab:
----
I didn't review this much after I wrote it, so I might make edits after the fact, and also I may have literally left a sentence out or something. Ping me if you want me to make a change!
Thanks for reading!