Skip to content

Commit d5f4eae

Browse files
committed
Limitation of closures returning futures
1 parent 43891ce commit d5f4eae

1 file changed

Lines changed: 137 additions & 1 deletion

File tree

src/part-guide/more-async-await.md

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,145 @@ A function which returns an async block is pretty similar to an async function.
133133

134134
You would usually prefer the async function version since it is simpler and clearer. However, the async block version is more flexible since you can execute some code when the function is called (by writing it outside the async block) and some code when the result is awaited (the code inside the async block).
135135

136-
137136
## Async closures
138137

138+
Closures are anonymous functions that capture data from their environment. They provide expressiveness by allowing passing behavior as a value in functional style APIs, notable examples of which include various methods on `Iterator` (e.g., `filter`, `map`, etc.) or spawning a thread with `std::thread::spawn`.
139+
140+
If a closure needs to await an async operation in its body, it has to return a future (just like async functions). A simple way is to return an async block from the closure, for example `|| async {}`. This often works, but because futures capture the data they use, closures returning futures don't work in all situations:
141+
142+
```rust,norun
143+
#[tokio::main]
144+
async fn main() {
145+
let mut logs = Vec::new();
146+
run(|line| async {
147+
logs.push(line);
148+
});
149+
}
150+
151+
async fn run<F, Fut>(f: F)
152+
where
153+
F: FnMut(&str) -> Fut,
154+
Fut: Future<Output = ()>,
155+
{
156+
todo!()
157+
}
158+
```
159+
160+
```
161+
error: captured variable cannot escape `FnMut` closure body
162+
--> examples/async_closure.rs:6:12
163+
|
164+
5 | let mut logs = Vec::new();
165+
| -------- variable defined here
166+
6 | run(|| async {
167+
| __________-_^
168+
| | |
169+
| | inferred to be a `FnMut` closure
170+
7 | | logs.push("complete".to_string());
171+
| | ---- variable captured here
172+
8 | | });
173+
| |_____^ returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
174+
|
175+
= note: `FnMut` closures only have access to their captured variables while they are executing...
176+
= note: ...therefore, they cannot allow references to captured variables to escape
177+
```
178+
179+
The problem is that `line` and `logs` are captured by the closure and only available during the execution of the closure, but the returned future needs to reference them when it is later awaited. And "later" is not soon enough, as it may be after the closure finishes execution. As a helpful mental model, you can think of the future created by the async block as a struct that implements the `Future` trait and is generic over the lifetime of the data it borrows.
180+
181+
```rust,norun
182+
#[tokio::main]
183+
async fn main() {
184+
let mut logs = Vec::new();
185+
run(|line| AsyncBlock {
186+
line,
187+
logs: &mut logs,
188+
});
189+
}
190+
191+
struct AsyncBlock<'line, 'logs> {
192+
line: &'line str,
193+
logs: &'logs mut Vec<String>,
194+
}
195+
196+
impl<'line, 'logs> Future for AsyncBlock<'line, 'logs> {
197+
type Output = ();
198+
199+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
200+
todo!()
201+
}
202+
}
203+
```
204+
205+
Here, the returned `AsyncBlock` value holds on to `line` and `logs`, which are only accessible to the closure while it executes.
206+
207+
However, that is not the only issue. Let's replace our inline closure with an actual async function and pass it to `run`. Note that the bound on `F` has also changed to `for<'a> FnMut(&'a str) -> Fut`, meaning that for all lifetimes `'a`, `F` is a closure accepting a `&str` that lives for `'a`. This is called a Higher-Rank Trait Bound (HRTB) in Rust, and `F` is said to be higher-ranked over the lifetime of its input.
208+
209+
```rust,norun
210+
#[tokio::main]
211+
async fn main() {
212+
run(do_something);
213+
}
214+
215+
async fn do_something(s: &str) {}
216+
217+
async fn run<F, Fut>(f: F)
218+
where
219+
F: for<'a> FnMut(&'a str) -> Fut,
220+
// ^------
221+
// HRTB used here
222+
Fut: Future<Output = ()>,
223+
{
224+
todo!()
225+
}
226+
```
227+
228+
```
229+
error: implementation of `FnMut` is not general enough
230+
--> examples/async_closure.rs:8:5
231+
|
232+
8 | run(do_something);
233+
| ^^^^^^^^^^^^^^^^^
234+
...
235+
13 | / async fn run<F, Fut>(f: F)
236+
14 | | where
237+
15 | | F: for<'a> FnMut(&'a str) -> Fut,
238+
| | ----------------------------- doesn't satisfy where-clause
239+
16 | | Fut: Future<Output = ()>,
240+
| |_____________________________- due to a where-clause on `run`...
241+
|
242+
= note: ...`for<'a> fn(&'a str) -> impl Future<Output = ()> {do_something}` must implement `FnMut<(&'a str,)>`
243+
= note: ...but it actually implements `FnMut<(&'0 str,)>`, for some specific lifetime `'0`
244+
```
245+
246+
This happens because the concrete types for `F` and `Fut` are determined by the caller, and there is no way to express that `Fut` may capture the higher-ranked lifetime `'a`. In `main`, `Fut` is inferred to be the future produced by `do_something`, which must capture the lifetime of its `&str` input to use it, but `for<'a>` requires `F` to work for any lifetime, not just the one tied to that specific input.
247+
248+
If your implementation doesn't need to reference its input `s`, you can use [opaque type precise capturing](https://doc.rust-lang.org/nightly/edition-guide/rust-2024/rpit-lifetime-capture.html) to indicate that:
249+
250+
```rust
251+
fn do_something(s: &str) -> impl Future<Output = ()> + use<> {
252+
async {}
253+
}
254+
255+
async fn run<F, Fut>(f: F)
256+
where
257+
F: for<'a> FnMut(&'a str) -> Fut,
258+
Fut: Future<Output = ()>,
259+
{
260+
todo!()
261+
}
262+
```
263+
264+
We desugar the async function to a regular sync function that returns a `Future` value. The `use<>` syntax tells the compiler that the opaque type should not capture any generic lifetime parameters, because the corresponding hidden type does not use them (notice that we return an empty async block).
265+
266+
Of course, you can't always avoid holding on to the input references, so the issue still remains.
267+
268+
As we have seen, to quote [the RFC for async closures](https://rust-lang.github.io/rfcs/3668-async-closures.html#motivation), two major limitations when using closures in async code includes
269+
270+
> 1. That closures cannot return futures that borrow from the closure captures.
271+
> 2. The inability to express higher-ranked async function signatures.
272+
273+
274+
#### TODO
139275
- closures
140276
- coming soon (https://github.com/rust-lang/rust/pull/132706, https://blog.rust-lang.org/inside-rust/2024/08/09/async-closures-call-for-testing.html)
141277
- async blocks in closures vs async closures

0 commit comments

Comments
 (0)