Skip to content

Commit 59e0eb2

Browse files
More writing
1 parent 88faa5b commit 59e0eb2

File tree

1 file changed

+35
-3
lines changed

1 file changed

+35
-3
lines changed

text/3668-async-closure.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ let _: Box<dyn async Fn()> = todo!();
276276

277277
All currently-stable callable types (i.e., closures, function items, function pointers, and `dyn Fn*` trait objects) automatically implement `async Fn*() -> T` if they implement `Fn*() -> Fut` for some output type `Fut`, and `Fut` implements `Future<Output = T>`.
278278

279-
This is to make sure that `async Fn*()` trait bounds have maximum compatibility with existing callable types which return futures, such as async function items and closures which return boxed futures. Async closures also implement `async Fn*()`, but their relationship to this trait is detailed later in the RFC.
279+
This is to make sure that `async Fn*()` trait bounds have maximum compatibility with existing callable types which return futures, such as async function items and closures which return boxed futures. Async closures also implement `async Fn*()`, but their relationship to this trait is detailed later in the RFC -- specifically the relationship between the `CallRefFuture` and `CallOnceFuture` associated types.
280280

281281
These implementations are built-in, but can conceptually be understood as:
282282

@@ -406,7 +406,9 @@ let _ = async move || {
406406

407407
#### Specifics about the `AsyncFnOnce` implementation, interaction with `move`
408408

409-
If the closure is inferred to be `async Fn` or `async FnMut`, then the compiler will synthesize an `async FnOnce` implementation for the closure which returns a future that doesn't borrow any captured values from the closure, but instead *moves* the captured values into the future.
409+
If the closure is inferred to be `async Fn` or `async FnMut`, then the compiler will synthesize an `async FnOnce` implementation for the closure which returns a future that doesn't borrow any captured values from the closure, but instead *moves* the captured values into the future. Synthesizing a distinct future that is returned by `async FnOnce` is necessary because the trait *consumes* the closure when it is called (evident from the `self` receiver type in the method signature), meaning that a self-borrowing future would have references to dropped data. This is an interesting problem described in more detail in [compiler-errors' blog post written on async closures][blog post].
410+
411+
This is reflected in the fact that `AsyncFnOnce::CallOnceFuture` is a distinct type from `AsyncFnMut::CallRefFuture`. While the latter is a generic-associated-type (GAT) due to supporting self-borrows of the called async closure, the former is not, since it must own all of the captures mentioned in the async closures' body.
410412

411413
For example:
412414

@@ -432,6 +434,8 @@ fut.await;
432434
// point, the allocation for `s` is dropped.
433435
```
434436

437+
Importantly, although these are distinct futures, they still have the same `Output` type (in other words, their futures await to the same type), and for types that have `async Fn*` implementations, the two future types *execute* identically, since they execute the same future body. They only differ in their captures.
438+
435439
### Interaction with return-type notation, naming the future returned by calling
436440

437441
With `async Fn() -> T` trait bounds, we don't know anything about the `Future` returned by calling the async closure other than that it's a `Future` and awaiting that future returns `T`.
@@ -478,7 +482,11 @@ This bound is only valid if there is a corresponding `async Fn*()` trait bound.
478482

479483
### Why do we need a new set of `AsyncFn*` traits?
480484

481-
As demonstrated in the motivation section, we need a set of traits that are *lending* in order to represent futures which borrow from the closure's captures. We technically only need to add `LendingFn` and `LendingFnMut` to our lattice of `Fn*` traits, leaving us with a hierarchy of traits like so:
485+
As demonstrated in the motivation section, we need a set of traits that are *lending* in order to represent futures which borrow from the closure's captures. This is described in more detail in [a blog post written on async closures][blog post].
486+
487+
[blog post]: https://hackmd.io/@compiler-errors/async-closures
488+
489+
We technically only need to add `LendingFn` and `LendingFnMut` to our lattice of `Fn*` traits to support the specifics about async closures' self-borrowing pattern, leaving us with a hierarchy of traits like so:
482490

483491
```mermaid
484492
flowchart LR
@@ -498,6 +506,16 @@ Fn -- isa --> LendingFn
498506
FnMut -- isa --> LendingFnMut
499507
```
500508

509+
In this case, `async Fn()` would desugar to a `LendingFnMut` trait bound and a `FnOnce` trait bound, like:
510+
511+
```rust
512+
where F: async Fn() -> i32
513+
514+
// is
515+
516+
where F: for<'s> LendingFn<LendingOutput<'s>: Future<Output = i32>> + FnOnce<Output: Future<Output = i32>>
517+
```
518+
501519
However, there are some concrete technical implementation details that limit our ability to use `LendingFn` ergonomically in the compiler today. These have to do with:
502520

503521
- Closure signature inference.
@@ -623,3 +641,17 @@ let handlers: HashMap<Id, Box<dyn async Fn()>> = todo!();
623641
```
624642

625643
This work will likely take a similar approach to making `async fn` in traits object-safe, since the major problem is how to "erase" the future returned by the async closure or callable, which differs for each implementation of the trait.
644+
645+
### Changing the underlying definition to use `LendingFn*`
646+
647+
As mentioned above, `async Fn*()` trait bounds can be adjusted to desugar to `LendingFn*` + `FnOnce` trait bounds, using associated-type-bounds like:
648+
649+
```rust
650+
where F: async Fn() -> i32
651+
652+
// desugars to
653+
654+
where F: for<'s> LendingFn<LendingOutput<'s>: Future<Output = i32>> + FnOnce<Output: Future<Output = i32>>
655+
```
656+
657+
This should be doable in a way that does not affect existing code, but remain blocked on improvements to higher-ranked trait bounds around [GATs](https://blog.rust-lang.org/2022/10/28/gats-stabilization.html#when-gats-go-wrong---a-few-current-bugs-and-limitations). Any changes along these lines remain implementation details unless we decide separately to stabilize more user-observable aspects of the `AsyncFn*` trait, which is not likely to happen soon.

0 commit comments

Comments
 (0)