Skip to content

Commit f7dca70

Browse files
Author "Async Closures MVP: Call for Testing!" (#1377)
Co-authored-by: Travis Cross <[email protected]>
1 parent f57626b commit f7dca70

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
layout: post
3+
title: "Async Closures MVP: Call for Testing!"
4+
author: Michael Goulet
5+
team: The Async Working Group <https://www.rust-lang.org/governance/wgs/wg-async>
6+
---
7+
8+
The async working group is excited to announce that [RFC 3668] "Async Closures" was recently approved by the Lang team. In this post, we want to briefly motivate why async closures exist, explain their current shortcomings, and most importantly, announce a call for testing them on nightly Rust.
9+
10+
## The backstory
11+
12+
Async closures were originally proposed in [RFC 2394](https://rust-lang.github.io/rfcs/2394-async_await.html#async--closures) which introduced `async`/`await` to the language. Simple handling of async closures has existed in nightly since async-await was implemented [soon thereafter](https://github.com/rust-lang/rust/pull/51580), but until recently async closures simply desugared into closures that returned async blocks:
13+
14+
```rust
15+
let x = async || {};
16+
17+
// ...was just sugar for:
18+
let x = || { async {} };
19+
```
20+
21+
This had a fundamental limitation that it was impossible to express a closure that returns a future that borrows captured state.
22+
23+
Somewhat relatedly, on the callee side, when users want to take an async closure as an argument, they typically express that as a bound of two different generic types:
24+
25+
```rust
26+
fn async_callback<F, Fut>(callback: F)
27+
where
28+
F: FnOnce() -> Fut,
29+
Fut: Future<Output = String>;
30+
```
31+
32+
This also led to an additional limitation that it's impossible to express higher-ranked async fn bounds using this without boxing (since a higher-ranked trait bound on `F` cannot lead to a higher-ranked type for `Fut`), leading to unnecessary allocations:
33+
34+
```rust
35+
fn async_callback<F>(callback: F)
36+
where
37+
F: FnOnce(&str) -> Pin<Box<dyn Future<Output = ()> + '_>>;
38+
39+
async fn do_something(name: &str) {}
40+
41+
async_callback(|name| Box::pin(async {
42+
do_something(name).await;
43+
}));
44+
```
45+
46+
These limitations were detailed in [Niko's blog post on async closures and lending](https://smallcultfollowing.com/babysteps/blog/2023/05/09/giving-lending-and-async-closures/#async-closures-are-a-lending-pattern), and later in compiler-errors's blog post on [why async closures are the way they are](https://hackmd.io/@compiler-errors/async-closures).
47+
48+
## OK, so how does [RFC 3668] help?
49+
50+
Recent [work](https://github.com/rust-lang/rust/pull/120361) has focused on reimplementing async closures to be lending and designing a set of async fn traits. While async closures already existed as syntax, this work introduced a new family of async fn traits which are implemented by async closures (and all other callable types which return futures). They can be written like:
51+
52+
```rust
53+
fn test<F>(callback: F)
54+
where
55+
// Either:
56+
async Fn(Arg, Arg) -> Ret,
57+
// Or:
58+
AsyncFn(Arg, Arg) -> Ret,
59+
```
60+
61+
(It's currently an [open question](https://github.com/rust-lang/rust/issues/128129) exactly how to spell this bound, so both syntaxes are implemented in parallel.)
62+
63+
RFC 3668 motivates this implementation work in detail, confirming that we need first-class async closures and async fn traits which allow us to express the *lending* capability of async closures -- read the RFC if you're interested in the whole story!
64+
65+
## So how do I help?
66+
67+
We'd love for you to test out these new features! First, on a recently-updated nightly compiler, enable `#![feature(async_closure)]` (note that, for historical reasons, this feature name is not pluralized).
68+
69+
Async closures are designed to be drop-in compatible (in almost all cases) with closures returning async blocks:
70+
71+
```rust
72+
// Instead of writing:
73+
takes_async_callback(|arg| async {
74+
// Do things here...
75+
});
76+
77+
// Write this:
78+
takes_async_callback(async |arg| {
79+
// Do things here...
80+
});
81+
```
82+
83+
And on the callee side, write async fn trait bounds instead of writing "regular" fn trait bounds that return futures:
84+
85+
```rust
86+
// Instead of writing:
87+
fn doesnt_exactly_take_an_async_closure<F, Fut>(callback: F)
88+
where
89+
F: FnOnce() -> Fut,
90+
Fut: Future<Output = String>
91+
{ todo!() }
92+
93+
// Write this:
94+
fn takes_an_async_closure<F: async FnOnce() -> String>(callback: F) { todo!() }
95+
// Or this:
96+
fn takes_an_async_closure<F: AsyncFnOnce() -> String>(callback: F) { todo!() }
97+
```
98+
99+
Or if you're emulating a higher-ranked async closure with boxing:
100+
101+
```rust
102+
// Instead of writing:
103+
fn higher_ranked<F>(callback: F)
104+
where
105+
F: Fn(&Arg) -> Pin<Box<dyn Future<Output = ()> + '_>>
106+
{ todo!() }
107+
108+
// Write this:
109+
fn higher_ranked<F: async Fn(&Arg)> { todo!() }
110+
// Or this:
111+
fn higher_ranked<F: AsyncFn(&Arg)> { todo!() }
112+
```
113+
114+
## Shortcomings interacting with the async ecosystem
115+
116+
If you're going to try to rewrite your async projects, there are a few shortcomings to be aware of.
117+
118+
### You can't directly name the output future
119+
120+
When you name an async callable bound with the *old* style, before first-class async fn trait bounds, then as a side-effect of needing to use two type parameters, you can put additional bounds (e.g. `+ Send` or `+ 'static`) on the `Future` part of the bound, like:
121+
122+
```rust
123+
fn async_callback<F, Fut>(callback: F)
124+
where
125+
F: FnOnce() -> Fut,
126+
Fut: Future<Output = String> + Send + 'static
127+
{ todo!() }
128+
```
129+
130+
There isn't currently a way to put similar bounds on the future returned by calling an async closure, so if you need to constrain your callback futures like this, then you won't be able to use async closures just yet.
131+
132+
We expect to support this in the medium/long term via a [return-type-notation syntax](https://rust-lang.github.io/rfcs/3668-async-closures.html#interaction-with-return-type-notation-naming-the-future-returned-by-calling).
133+
134+
### Subtle differences in closure signature inference
135+
136+
Passing an async closure to a generic `impl Fn(A, B) -> C` bound may not always eagerly infer the closure's arguments to `A` and `B`, leading to strange type errors on occasion. For an example of this, see [`rust-lang/rust#127781`](https://github.com/rust-lang/rust/issues/127781).
137+
138+
We expect to improve async closure signature inference as we move forward.
139+
140+
### Async closures can't be coerced to `fn()` pointers
141+
142+
Some libraries take their callbacks as function *pointers* (`fn()`) rather than generics. Async closures don't currently implement the same coercion from closure to `fn() -> ...`. Some libraries may mitigate this problem by adapting their API to take generic `impl Fn()` instead of `fn()` pointers as an argument.
143+
144+
We don't expect to implement this coercion unless there's a particularly good reason to support it, since this can usually be handled manually by the caller by using an inner function item, or by using an `Fn` bound instead, for example:
145+
146+
```rust
147+
fn needs_fn_pointer<T: Future<Output = ()>>(callback: fn() -> T) { todo!() }
148+
149+
fn main() {
150+
// Instead of writing:
151+
needs_fn_pointer(async || { todo!() });
152+
// Since async closures don't currently support coercion to `fn() -> ...`.
153+
154+
// You can use an inner async fn item:
155+
async fn callback() { todo!() }
156+
needs_fn_pointer(callback);
157+
}
158+
159+
// Or if you don't need to take *exactly* a function pointer,
160+
// you can rewrite `needs_fn_pointer` like:
161+
fn needs_fn_pointer(callback: impl async Fn()) { todo!() }
162+
// Or with `AsyncFn`:
163+
fn needs_fn_pointer(callback: impl AsyncFn()) { todo!() }
164+
```
165+
166+
[RFC 3668]: https://rust-lang.github.io/rfcs/3668-async-closures.html

0 commit comments

Comments
 (0)