|
| 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 | +The async working group is excited to announce that `async fn` can now be used in traits in the nightly compiler. You can now write code like this: |
| 9 | + |
| 10 | +```rust |
| 11 | +#![feature(async_fn_in_trait)] |
| 12 | + |
| 13 | +trait Database { |
| 14 | + async fn fetch_data(&self) -> String; |
| 15 | +} |
| 16 | + |
| 17 | +impl Database for MyDb { |
| 18 | + async fn fetch_data(&self) -> String { ... } |
| 19 | +} |
| 20 | +``` |
| 21 | + |
| 22 | +A full working example is available in the [playground][play-concrete-spawn]. There are some limitations we'll cover, as well as a few known bugs to be worked out, but we think it is ready for some users try. Read on for the specifics. |
| 23 | + |
| 24 | +## Recap: How async/await works in Rust |
| 25 | + |
| 26 | +`async` and `.await` were a major improvement in the ergonomics of writing async code in Rust. In Rust, an `async fn` returns a `Future`, which is some object that represents an ongoing asynchronous computation. |
| 27 | + |
| 28 | +The type of the future does not actually appear in the signature of an `async fn`. When you write an async function like this: |
| 29 | + |
| 30 | +```rust |
| 31 | +async fn fetch_data(db: &MyDb) -> String { ... } |
| 32 | +``` |
| 33 | + |
| 34 | +The compiler rewrites it to something like this: |
| 35 | + |
| 36 | +```rust |
| 37 | +fn fetch_data<'a>(db: &'a MyDb) -> impl Future<Output = String> + 'a { |
| 38 | + async move { ... } |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +This "desugared" signature is something you can write yourself, and it's useful for examining what goes on under the hood. The `impl Future` syntax here represents some _opaque type_ that implements `Future`. |
| 43 | + |
| 44 | +The future is a state machine responsible for knowing how to continue making progress the next time it wakes up. When you write code in an `async` block, the compiler generates a future type specific to that async block for you. This future type does not have a name, so we must instead use an opaque type in the function signature. |
| 45 | + |
| 46 | +## The historic problem of `async fn` in trait |
| 47 | + |
| 48 | +Traits are the fundamental mechanism of abstraction in Rust. So what happens if you want to put an async method in a trait? Each `async` block or function creates a unique type, so you would want to express that each implementation can have a different Future for the return type. Thankfully, we have associated types for this: |
| 49 | + |
| 50 | +```rust |
| 51 | +trait Database { |
| 52 | + type FetchData<'a>: Future<Output = String> + 'a where Self: 'a; |
| 53 | + fn fetch_data(&self) -> FetchData<'a>; |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +Notice that this associated type is generic. Generic associated types haven't been supported in the language... [until now][GATs]! Unfortunately though, even with GATs, you still can't write a trait _implementation_ that uses `async`: |
| 58 | + |
| 59 | +```rust |
| 60 | +impl Database for MyDb { |
| 61 | + type FetchData<'a> = /* what type goes here??? */; |
| 62 | + fn fetch_data(&self) -> FetchData<'a> { async move { ... } } |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +Since you can't name the type constructed by an async block, the only option is to use an opaque type (the `impl Future` we saw earlier). But those are not supported in associated types![^tait] |
| 67 | + |
| 68 | +[^tait]: This feature is called ["type alias impl trait"](https://rust-lang.github.io/rfcs/2515-type_alias_impl_trait.html). |
| 69 | + |
| 70 | +### Workarounds available in the stable compiler |
| 71 | + |
| 72 | +So to write an `async fn` in a trait we need a concrete type to specify in our impl. There are a couple ways of achieving this today. |
| 73 | + |
| 74 | +#### Runtime type erasure |
| 75 | + |
| 76 | +First, we can avoid writing the future type by erasing it with `dyn`. Taking our example from above, you would write your trait like this: |
| 77 | + |
| 78 | +```rust |
| 79 | +trait Database { |
| 80 | + fn fetch_data(&self) |
| 81 | + -> Pin<Box<dyn Future<Output = String> + Send + '_>>; |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +This is significantly more verbose, 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 |
| 86 | + |
| 87 | +```rust |
| 88 | +#[async_trait] |
| 89 | +trait Database { |
| 90 | + async fn fetch_data(&self) -> String; |
| 91 | +} |
| 92 | + |
| 93 | +#[async_trait] |
| 94 | +impl Database for MyDb { |
| 95 | + async fn fetch_data(&self) -> String { ... } |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +This is an excellent solution for the people who can use it! |
| 100 | + |
| 101 | +Unfortunately, not everyone can. You can't use `Box` in no_std contexts. Dynamic dispatch and allocation come with overhead that can be [overwhelming][barbara-benchmark] for 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. |
| 102 | + |
| 103 | +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. |
| 104 | + |
| 105 | +#### Manual `poll` implementations |
| 106 | + |
| 107 | +Traits that need to work with zero overhead or in no_std contexts have another option: they can take the concept of polling from the [`Future` trait](https://doc.rust-lang.org/stable/std/future/trait.Future.html) and build it directly into their interface. The `Future::poll` method returns `Poll::Ready(Output)` if the future is complete and `Poll::Pending` if the future is waiting on some other event. |
| 108 | + |
| 109 | +You can see this pattern, for example, in the current version of the unstable [AsyncIterator](https://doc.rust-lang.org/stable/std/async_iter/trait.AsyncIterator.html) trait. The signature of `AsyncIterator::poll_next` is a cross between `Future::poll` and `Iterator::next`. |
| 110 | + |
| 111 | +```rust |
| 112 | +pub trait AsyncIterator { |
| 113 | + type Item; |
| 114 | + |
| 115 | + fn poll_next( |
| 116 | + self: Pin<&mut Self>, |
| 117 | + cx: &mut Context<'_> |
| 118 | + ) -> Poll<Option<Self::Item>>; |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +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. |
| 123 | + |
| 124 | +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. |
| 125 | + |
| 126 | +## What's supported in nightly |
| 127 | + |
| 128 | +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. |
| 129 | + |
| 130 | +First, as you might expect, you can write and implement traits just like the above. |
| 131 | + |
| 132 | +```rust |
| 133 | +#![feature(async_fn_in_trait)] |
| 134 | + |
| 135 | +trait Database { |
| 136 | + async fn fetch_data(&self) -> String; |
| 137 | +} |
| 138 | + |
| 139 | +impl Database for MyDb { |
| 140 | + async fn fetch_data(&self) -> String { ... } |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +One thing this will allow us to do is standardize new traits we've been waiting on this feature for. For example, the `AsyncIterator` trait from above is significantly more complicated than its analogue, `Iterator`. With the new support, we can simply write this instead: |
| 145 | + |
| 146 | +```rust |
| 147 | +#![feature(async_fn_in_trait)] |
| 148 | + |
| 149 | +trait AsyncIterator { |
| 150 | + type Item; |
| 151 | + async fn next(&mut self) -> Option<Self::Item>; |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +There's a decent chance that the trait in the standard library will end up exactly like this! For now, you can also use the one in the [`async_iterator` crate](https://docs.rs/async-iterator/latest/async_iterator/) and write generic code with it, just like you would normally. |
| 156 | + |
| 157 | +```rust |
| 158 | +async fn print_all<I: AsyncIterator>(mut count: I) |
| 159 | +where |
| 160 | + I::Item: Display, |
| 161 | +{ |
| 162 | + while let Some(x) = count.next().await { |
| 163 | + println!("{x}"); |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### Limitation: Spawning from generics |
| 169 | + |
| 170 | +There is a catch! If you try to *spawn* from a generic function like `print_all`, and (like the majority of 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] |
| 171 | + |
| 172 | +```rust |
| 173 | +fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I) |
| 174 | +where |
| 175 | + I::Item: Display, |
| 176 | +{ |
| 177 | + tokio::spawn(async move { |
| 178 | + // ^^^^^^^^^^^^ |
| 179 | + // ERROR: future cannot be sent between threads safely |
| 180 | + while let Some(x) = count.next().await { |
| 181 | + // ^^^^^^^^^^^^ |
| 182 | + // note: future is not `Send` as it awaits another future which is not `Send` |
| 183 | + println!("{x}"); |
| 184 | + } |
| 185 | + }); |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +[^actual-error]: The actual error message produced by the compiler is a bit noisier than this, but that will be improved. |
| 190 | + |
| 191 | +You can see that we added an `I: Send` bound in the function signature, but that was not enough. To satisfy this error we need to say that the *future returned by `next()`* is `Send`. But as we saw at the beginning of this post, async functions return anonymous types. There's no way to express bounds on those types. |
| 192 | + |
| 193 | +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. |
| 194 | + |
| 195 | +#### Hypothesis: This is uncommon |
| 196 | + |
| 197 | +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]: |
| 198 | + |
| 199 | +```rust |
| 200 | +async fn do_something() { |
| 201 | + let iter = Countdown::new(10); |
| 202 | + executor::spawn(print_all(iter)); |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +[play-concrete-spawn]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=6ffde69ba43c6c5094b7fbdae11774a9 |
| 207 | + |
| 208 | +This works without any `Send` bounds whatsoever! This works because `do_something` knows the concrete type of our iterator, `Countdown`. The compiler knows that this type is `Send`, and that `print_all(iter)` therefore produces a future that is `Send`.[^auto-traits-special] |
| 209 | + |
| 210 | +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. |
| 211 | + |
| 212 | +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]. |
| 213 | + |
| 214 | +#### When it does happen |
| 215 | + |
| 216 | +Eventually you probably *will* want to spawn from a context that's generic over an async trait that you call. What then!? |
| 217 | + |
| 218 | +For now it's possible to use another new nightly-only feature, `return_position_impl_trait_in_trait`, to express the bound you need directly in your trait: |
| 219 | + |
| 220 | +```rust |
| 221 | +#![feature(return_position_impl_trait_in_trait)] |
| 222 | + |
| 223 | +trait SpawnAsyncIterator: Send + 'static { |
| 224 | + type Item; |
| 225 | + fn next(&mut self) -> impl Future<Output = Option<Self::Item>> + Send + '_; |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +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. |
| 230 | + |
| 231 | +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. |
| 232 | + |
| 233 | +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. |
| 234 | + |
| 235 | +### Limitation: Dynamic dispatch |
| 236 | + |
| 237 | +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 better off using the `async_trait` macro for now. |
| 238 | + |
| 239 | +## Path to stabilization |
| 240 | + |
| 241 | +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). |
| 242 | + |
| 243 | +[^stabilization-when]: When? Possibly sometime in the next six months or so. But don't hold me to it :) |
| 244 | + |
| 245 | +There are two big questions to answer first: |
| 246 | + |
| 247 | +* **Do we need to solve the "spawning from generics" (`Send` bound) problem first?** Please leave feedback on [this issue][send-bound-issue]. |
| 248 | +* **What other bugs and quality issues exist?** Please file [new issues](https://github.com/rust-lang/rust/issues/new/choose) for these. You can view [known issues here](https://github.com/rust-lang/rust/labels/F-async_fn_in_trait). |
| 249 | + |
| 250 | +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! |
| 251 | + |
| 252 | +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. |
| 253 | + |
| 254 | +## Conclusion |
| 255 | + |
| 256 | +This work was made possible thanks to the efforts of many people, including |
| 257 | + |
| 258 | +* Michael Goulet |
| 259 | +* Santiago Pastorino |
| 260 | +* Oli Scherer |
| 261 | +* Eric Holk |
| 262 | +* Dan Johnson |
| 263 | +* Bryan Garza |
| 264 | +* Niko Matsakis |
| 265 | +* Tyler Mandry |
| 266 | + |
| 267 | +In addition it was built on top of years of compiler work that enabled us to ship [GATs] as well as other fundamental type system improvements. We're deeply grateful to all those who contributed; this work would not be possible without you. Thank you! |
| 268 | + |
| 269 | +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. |
| 270 | + |
| 271 | + |
| 272 | +_Thanks to Yoshua Wuyts, Nick Cameron, Dan Johnson, Santiago Pastorino, Eric Holk, and Niko Matsakis for reviewing a draft of this post._ |
| 273 | + |
| 274 | + |
| 275 | +[^auto-traits-special]: Auto traits such as `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 type of its argument `Send`, and unlike with regular traits, it is allowed to use this knowledge when type checking your program. |
| 276 | +[^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/). |
| 277 | + |
| 278 | +[initial-impl]: https://github.com/rust-lang/rust/pull/101224 |
| 279 | +[GATs]: https://blog.rust-lang.org/2022/10/28/gats-stabilization.html |
| 280 | +[RFC]: https://rust-lang.github.io/rfcs/3185-static-async-fn-in-trait.html |
| 281 | +[why async fn in traits are hard]: https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/ |
| 282 | +[async-trait]: https://crates.io/crates/async-trait |
| 283 | +[vision-blog]: https://blog.rust-lang.org/2021/03/18/async-vision-doc.html |
| 284 | +[alan-stream]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/alan_hates_writing_a_stream.html |
| 285 | +[alan-async-traits]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/alan_needs_async_in_traits.html |
| 286 | +[barbara-mutex]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/barbara_polls_a_mutex.html |
| 287 | +[barbara-benchmark]: https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/barbara_benchmarks_async_trait.html |
| 288 | +[send-bound-issue]: https://github.com/rust-lang/rust/issues/103854 |
0 commit comments