This post attempts to motivate some of the concrete technical reasons why the #![feature(async_closure)]
that I've been working on recently[1][2][3][4] 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.
You may have tried in the past to make your closures async by writing a regular closure that returns an async
block:
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:
/// 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[5]. So, let's try that again:
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[6], and call it in our closure:
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.
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?
This limitation is related to an interesting blog post that Niko wrote a while back: "Giving, lending, and async closures"[7].
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:
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
Ok, let's try these definitions out. First, let's express our query_all_services
function with LendingFnMut
:
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 syntax that I recently stabilized, 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)]
:
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…
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[8]:
[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 if you're curious. I'll summarize the gist of the issue in just a second. First, let's think about this more abstractly.
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:
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:
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. 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?
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:
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:
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:
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>
, repr(transparent)
-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
FnOnce
implementationSo 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.
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
[9] and turns it into a future that is compatible with FnOnce
: by_move_body.rs
in the compiler. You may also want to read here and here in the upvar.rs
capture analysis code in the type checker.
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
. Specifically, we have:
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:
LendingFnMut
.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:
// 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:
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:
type Output;
And both the AsyncFnOnce
and AsyncFnMut
implementation share this output type, even if they have different future types.
LendingFnMut
trait.LendingFnMut
is not easy to introduce into the current Fn->FnMut->FnOnce
trait hierarchy, because LendingFnMut
cannot be a subtrait of FnOnce
. Why? Because:
LendingFnMut
implementation typically differs from that returned by FnOnce
, but they're inherited by the current subtraits.FnOnce
at all.FnMut
and make it "just work".FnOnce
implementation that can be derived programmatically from their LendingFnMut
implementation.LendingFnMut
) and specialize this trait for async – we introduce AsyncFn
/AsyncFnMut
/AsyncFnOnce
.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
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!
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. ↩︎
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. ↩︎
You should read the section "Async closures are a lending pattern". ↩︎
the FnOnce
implementation for an FnMut
closure will just dispatch to the FnMut
impl. It's called the "FnOnce
shim" in rustc
parlance. ↩︎
Well, in reality it's AsyncFnMut
. More about that directly below. ↩︎