Skip to content

Commit 70e3f8a

Browse files
authored
Add post announcing async_fn_in_trait on nightly
cc @rust-lang/wg-async
1 parent 6045a54 commit 70e3f8a

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
---
2+
layout: post
3+
title: "Async fn in trait MVP comes to nightly"
4+
author: Tyler Mandry
5+
team: The Rust Async Working Group <https://www.rust-lang.org/governance/wgs/wg-async>
6+
---
7+
8+
`async` and `.await` were a major improvement in the ergonomics of writing async code in Rust. If you use async Rust for long, however, you'll surely discover a big limitation: `async fn` doesn't work in traits.
9+
10+
Traits are Rust's mechanism for writing generic code. How, you might reasonably ask, could we ship a feature that doesn't work in *traits*? As we'll see, this is a big pain point for users, but there are workarounds available. Let's go over the state of things today and then talk about what new features are available in the nightly compiler.
11+
12+
## Recap: The problem of `async fn` in trait
13+
14+
An `async fn` returns a `Future`, which is some object that represents an ongoing asynchronous computation.
15+
16+
However, the type of the future does not appear in the signature of an `async fn`. When you write an async function like this:
17+
18+
```rust
19+
impl MyDatabase {
20+
async fn fetch_data(&self) -> String { ... }
21+
}
22+
```
23+
24+
The compiler rewrites it to something like this:
25+
26+
```rust
27+
impl MyDatabase {
28+
fn fetch_data<'a>(&'a self) -> impl Future<Output = String> + 'a {
29+
async move { ... }
30+
}
31+
}
32+
```
33+
34+
The `impl Future` here is some _opaque type_ that implements `Future`. It must be opaque because the type is generated by the compiler and doesn't have a name. That type is *specific to the async block*.
35+
36+
If you wanted to do this in a trait, you'd be stuck because traits don't support returning opaque types. You might instead try to write it using an associated type:
37+
38+
```rust
39+
trait Database {
40+
type FetchData<'a>: Future<Output = String> + 'a where Self: 'a;
41+
fn fetch_data(&self) -> FetchData<'a>;
42+
}
43+
```
44+
45+
Notice that this associated type is generic. That hasn't been supported in the language [until now][GATs]. Unfortunately, even with GATs, you can't write an implementation that uses `async`:
46+
47+
```rust
48+
impl Database for MyDatabase {
49+
type FetchData<'a> = /* what goes here??? */;
50+
fn fetch_data(&self) -> FetchData<'a> { async move { ... } }
51+
}
52+
```
53+
54+
Since you can't name the type constructed by an async block, the only option is to use an opaque type. But those are not supported in type aliases, including associated types![^tait]
55+
56+
[^tait]: This feature is called ["type alias impl trait"](https://rust-lang.github.io/rfcs/2515-type_alias_impl_trait.html).
57+
58+
### Workarounds available today
59+
60+
So we need a concrete type to specify in our impl, if not our trait. There are a couple ways of achieving this today.
61+
62+
#### Runtime type erasure
63+
64+
We can avoid writing the future type by erasing it with `dyn`. Taking our example from above, you would write your trait like this:
65+
66+
```rust
67+
trait Database {
68+
fn fetch_data(&self)
69+
-> Pin<Box<dyn Future<Output = String> + Send + '_>>;
70+
}
71+
```
72+
73+
This is significantly more verbose (and it gets worse for implementers), but it achieves the goal of combining async with traits. What's more, the [async-trait] proc macro crate rewrites your code for you, allowing you to simply write
74+
75+
```rust
76+
#[async_trait]
77+
trait Database {
78+
async fn fetch_data(&self) -> String;
79+
}
80+
81+
#[async_trait]
82+
impl Database for MyDatabase {
83+
async fn fetch_data(&self) -> String { ... }
84+
}
85+
```
86+
87+
This is an excellent solution for the people who can use it! Unfortunately, not everyone can. It doesn't work in `no_std` contexts. Dynamic dispatch and allocation come with overhead that can be [crippling][barbara-benchmark] to highly performance-sensitive code. Finally, it bakes a lot of assumptions into the trait itself: allocation with `Box`, dynamic dispatch, and the `Send`-ness of the futures. This makes it unsuitable for many libraries.
88+
89+
So these workarounds leave something to be desired. Besides, users [expect][alan-async-traits] to be able to write `async fn` in traits, and the experience of adding an external crate dependency is a papercut that gives async Rust a reputation for being difficult to use.
90+
91+
#### Manual `poll` implementations
92+
93+
Traits that need to work with zero overhead or in no_std contexts had another option: they could build polling directly into their interface. For example, the `Stream` trait in the `futures` crate:
94+
95+
```rust
96+
pub trait Stream {
97+
type Item;
98+
99+
fn poll_next(
100+
self: Pin<&mut Self>,
101+
cx: &mut Context<'_>
102+
) -> Poll<Option<Self::Item>>;
103+
}
104+
```
105+
106+
This has the same signature as [`Future::poll`](https://doc.rust-lang.org/stable/std/future/trait.Future.html), a method that returns either `Poll::Ready(Output)` if the future is complete, or `Poll::Pending` if it is waiting on some other event. Effectively, a trait implementing `Stream` is like a special Future that you could continue to poll for the next item after receiving `Poll::Ready`.
107+
108+
Before async/await, it was very common to write manual `poll` implementations. Unfortunately, they proved challenging to write correctly. In the [vision document][vision-blog] process we underwent last year, we received a number of reports on how this was [extremely difficult][alan-stream] and a [source of bugs][barbara-mutex] for Rust users.
109+
110+
In fact, the difficulty of writing manual poll implementations was a primary reason for adding async/await to the core language in the first place.
111+
112+
## What's new
113+
114+
We've been working to support `async fn` directly in traits, and an implementation [recently landed][initial-impl] in nightly! The feature still has some rough edges, but let's take a look at what you can do with it.
115+
116+
First, as you might expect, you can write and implement traits just like the above.
117+
118+
```rust
119+
#![feature(async_fn_in_trait)]
120+
121+
trait Database {
122+
async fn fetch_data(&self) -> String;
123+
}
124+
125+
impl Database for MyDatabase {
126+
async fn fetch_data(&self) -> String { ... }
127+
}
128+
```
129+
130+
One thing this will allow us to do is standardize new traits we've been waiting on this feature for. For example, the `Stream` trait from above is significantly more complicated than its analogue, `Iterator`. With the new support, we can simply write this instead:
131+
132+
```rust
133+
#![feature(async_fn_in_trait)]
134+
135+
trait AsyncIterator {
136+
type Item;
137+
async fn next(&mut self) -> Option<Self::Item>;
138+
}
139+
```
140+
141+
There's a decent chance that exactly this trait will end up in the standard library. For now though, you can use the one in the [`async_iterator` crate](https://docs.rs/async-iterator/latest/async_iterator/) and use it in generic code, just like you would normally.
142+
143+
```rust
144+
async fn print_all<I: AsyncIterator>(mut count: I)
145+
where
146+
I::Item: Display,
147+
{
148+
while let Some(x) = count.next().await {
149+
println!("{x}");
150+
}
151+
}
152+
```
153+
154+
### Limitation: Spawning from generics
155+
156+
However, there is a catch! If you try to spawn from a generic function like `print_all`, and (like most async users), you use a work stealing executor that requires spawned tasks to be `Send`, you'll hit an error which is not easily resolved.[^actual-error]
157+
158+
```rust
159+
fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
160+
where
161+
I::Item: Display,
162+
{
163+
tokio::spawn(async move {
164+
// ^^^^^^^^^^^^
165+
// ERROR: future cannot be sent between threads safely
166+
while let Some(x) = count.next().await {
167+
// ^^^^^^^^^^^^
168+
// note: future is not `Send` as it awaits another future which is not `Send`
169+
println!("{x}");
170+
}
171+
});
172+
}
173+
```
174+
175+
[^actual-error]: The actual error message produced by the compiler is a bit noisier than this, but that can be improved.
176+
177+
Even though we added a `Send` bound on `I` in this function, it's not enough. We need to say that the *future* returned by `next()` is `Send`, but we can't: async functions return anonymous types, and you can't bound anonymous types in Rust.
178+
179+
There are potential solutions to this problem that we'll be exploring in a follow-up post. But for now, there are a couple things you can do to get out of a situation like this.
180+
181+
#### Hypothesis: This is uncommon
182+
183+
First, you *may* be surprised to find that this situation just doesn't occur that often in practice. For example, we can spawn a task that invokes the above `print_all` function [without any problem][play-concrete-spawn]:
184+
185+
```rust
186+
async fn do_something() {
187+
let iter = Countdown::new(10);
188+
executor::spawn(print_all(iter));
189+
}
190+
```
191+
[play-concrete-spawn]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=fc6b1ef2060ac8a79079ad8d59822727
192+
193+
This works without any `Send` bounds whatsoever! This works because the function we're spawning from knows the concrete type of our iterator, `Countdown`. The compiler knows that that type is `Send`, and that it returns a future that is `Send`.[^auto-traits-special]
194+
195+
One hypothesis is that while people will hit this problem, they will encounter it relatively infrequently, because most of the time `spawn` won't be called in code that's generic over a trait with async functions.
196+
197+
We would like to start gathering data on people's actual experiences with this. If you have relevant experience to share, [please comment on this issue][send-bound-issue].
198+
199+
#### When it does happen
200+
201+
Eventually you probably *will* want to spawn from a context that's generic over an async trait that you call. What then!?
202+
203+
For now it's possible to use another new nightly-only feature, `return_position_impl_trait_in_trait`, to express this directly in your trait:
204+
205+
```rust
206+
#![feature(return_position_impl_trait_in_trait)]
207+
208+
trait SpawnAsyncIterator: Send + 'static {
209+
type Item;
210+
fn next(&mut self) -> impl Future<Output = Option<Self::Item>> + Send + '_;
211+
}
212+
```
213+
214+
Here we've *desugared* our `async fn` to a regular function returning `impl Future + '_`, which works just like normal `async fn`s do. It's more verbose, but it gives us a place to put a `+ Send` bound! What's more, you can continue to use `async fn` in an `impl` of this trait.
215+
216+
The downside of this approach is that the trait becomes less generic, making it less suitable for library code. If you want to maintain two separate versions of a trait, you can do that, and (perhaps) provide macros to make it easier to implement both.
217+
218+
This solution is intended to be temporary. We'd like to implement a better solution in the language itself, but since this is a nightly-only feature we prefer to get more people trying it out as soon as possible.
219+
220+
### Limitation: Dynamic dispatch
221+
222+
There's one final limitation: You can't call an `async fn` with a `dyn Trait`. Designs to support this exist[^dyn-designs], but are in the earlier stages. If you need dynamic dispatch from a trait, you're much better off using the `async_trait` crate.
223+
224+
## Path to stabilization
225+
226+
The async working group would like to get something useful in the hands of Rust users, even if it doesn't do *everything* they might want. That's why despite having some limitations, the current version of `async fn` in traits might not be far off from stabilization.[^stabilization-when] You can follow progress by watching the [tracking issue](https://github.com/rust-lang/rust/issues/91611).
227+
228+
[^stabilization-when]: When? Possibly sometime in the next six months or so. But don't hold me to it :)
229+
230+
There are two big questions to answer first:
231+
232+
* **Do we need to solve the "spawning from generics" (`Send` bound) problem first?** Please leave feedback on [this issue][send-bound-issue].
233+
* **What other bugs and quality issues exist?** Please file [new issues](https://github.com/rust-lang/rust/issues/new/choose) for these. Also see [known issues](https://github.com/rust-lang/rust/labels/F-async_fn_in_trait).
234+
235+
If you're an async Rust enthusiast and are willing to try experimental new features, we'd very much appreciate it if you gave it a spin!
236+
237+
If you use `#[async_trait]`, you can try removing it from some traits (and their impls) where you don't use dynamic dispatch. Or if you're writing new async code, try using it there. Either way, you can tell us about your experience at the links above.
238+
239+
## Conclusion
240+
241+
This work was made possible thanks to the efforts of many people, including
242+
243+
* Michael Goulet
244+
* Santiago Pastorino
245+
* Oli Scherer
246+
* Eric Holk
247+
* Dan Johnson
248+
* Niko Matsakis
249+
* Tyler Mandry
250+
251+
It was built on top of years of compiler work that enabled us to ship [GATs] as well as other fundamental type system improvements.
252+
253+
To learn more about this feature and the challenges behind it, check out the [Static async fn in traits RFC][RFC] and [why async fn in traits are hard]. Also stay tuned for a follow-up post where we explore language extensions that make it possible to express `Send` bounds without a special trait.
254+
255+
256+
_Thanks to Yoshua Wuyts, Dan Johnson, Santiago Pastorino, and Eric Holk for reviewing a draft of this post._
257+
258+
259+
[^auto-traits-special]: But how does the compiler know that the future returned by `print_all`, the one we pass to `spawn`, is `Send`? Auto traits like `Send` and `Sync` are special in this way. The compiler knows that the return type of `print_all` is `Send` if and only if the conditions above are satisfied, and unlike with normal traits, it is allowed to use this knowledge when type checking your program.
260+
[^dyn-designs]: See [Async fn in dyn trait](https://rust-lang.github.io/async-fundamentals-initiative/explainer/async_fn_in_dyn_trait.html) on the initiative website, as well as posts 8 and 9 in [this series](https://smallcultfollowing.com/babysteps/blog/2022/09/21/dyn-async-traits-part-9-callee-site-selection/).
261+
262+
[initial-impl]: https://github.com/rust-lang/rust/pull/101224
263+
[GATs]: https://blog.rust-lang.org/2022/10/28/gats-stabilization.html
264+
[RFC]: https://rust-lang.github.io/rfcs/3185-static-async-fn-in-trait.html
265+
[why async fn in traits are hard]: https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/
266+
[async-trait]: https://crates.io/crates/async-trait
267+
[vision-blog]: https://blog.rust-lang.org/2021/03/18/async-vision-doc.html
268+
[alan-stream]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/alan_hates_writing_a_stream.html
269+
[alan-async-traits]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/alan_needs_async_in_traits.html
270+
[barbara-mutex]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/barbara_polls_a_mutex.html
271+
[barbara-benchmark]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/barbara_benchmarks_async_trait.html
272+
[send-bound-issue]: https://github.com/rust-lang/rust/issues/103854

0 commit comments

Comments
 (0)