Skip to content

Commit cccc284

Browse files
authored
Post: Stabilizing async fn in traits in 2023
1 parent 646fe43 commit cccc284

File tree

1 file changed

+197
-0
lines changed

1 file changed

+197
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
---
2+
layout: post
3+
title: "Stabilizing async fn in traits in 2023"
4+
author: Niko Matsakis and 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's headline goal for 2023 is to stabilize a "minimum viable product" (MVP) version of async functions in traits. We are currently targeting Rust 1.74 for stabilization. This post lays out the features we plan to ship and the status of each one.
9+
10+
In November, we [blogged about nightly support for async fn in trait][pp] and identified some key next steps, most importantly [support for send bounds to allow spawning in generic functions](https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-in-trait-nightly.html#limitation-spawning-from-generics). Since then we've done a significant amount of design exploration and collected a set of case studies evaluating how well the current code works in practice.
11+
12+
As of now, all of the functionality described in this blog post is implemented and available in some form on the nightly compiler. This was done to prove out the viability, but not all of it has been formally RFC'd. We'll link to playground examples and RFCs where appropriate so you can try it for yourself or read about the details.
13+
14+
[pp]: https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-in-trait-nightly.html
15+
16+
## MVP Part 1: Core support for "async functions in traits"
17+
18+
The easiest way to explain what we are going to stabilize is to use a code example. To start, we will permit the use of `async fn` in traits definitions...
19+
20+
```rust
21+
trait HealthCheck {
22+
async fn check(&mut self) -> bool;
23+
}
24+
```
25+
26+
...and you can then use `async fn` in the corresponding impl:
27+
28+
```rust
29+
impl HealthCheck for MyHealthChecker {
30+
async fn check(&mut self) -> bool {
31+
do_async_op().await
32+
}
33+
}
34+
```
35+
36+
Traits with async functions can then be used as you normally would:
37+
38+
```rust
39+
async fn do_health_check(hc: impl HealthCheck) {
40+
if !hc.check().await {
41+
log_health_check_failure().await;
42+
}
43+
}
44+
```
45+
46+
**Status:** This functionality was described in [RFC 3185], merged on Dec 7, 2021, and was covered in detail in our [previous blog post][pp].
47+
48+
[RFC 3185]: https://rust-lang.github.io/rfcs/3185-static-async-fn-in-trait.html
49+
50+
51+
## MVP Part 2: Send bounds and associated return types
52+
53+
There is one complication that arises when using async functions in traits that doesn't arise with sync functions. Many async runtimes -- notably including the default configurations of [Tokio] and [async-std] -- use a workstealing thread scheduler. This means that futures may move between worker threads dynamically to achieve load balancing. As a result, the future must only capture `Send` data.
54+
55+
[Tokio]: https://tokio.rs/
56+
57+
[async-std]: https://async.rs/
58+
59+
If you author a generic async function that spawns tasks on one of those runtimes, however, you will start to get compilation errors ([playground](XXX)):
60+
61+
```rust
62+
async fn do_health_check_par(hc: impl HealthCheck) {
63+
tokio::task::spawn(async move {
64+
if !hc.check().await {
65+
log_health_check_failure().await;
66+
}
67+
});
68+
}
69+
```
70+
71+
The problem is that the future returned by `hc.check()` isn't guaranteed to be `Send`. It might access non-Send data. The solution is to add a `Send` bound, but given that this is an async function, it's not obvious how to do that. How do we talk about the future returned by a call to `hc.check()`? Associated return types provide the answer. We can convert the above function to use an explicit type parameter `HC` (instead of `impl HealthCheck`) and then add a new bound, `HC::check(): Send`. This says "the value returned by `HC::check` must be of `Send` type":
72+
73+
```rust
74+
async fn do_health_check_par<HC>(hc: HC)
75+
where
76+
HC: HealthCheck + Send + 'static,
77+
HC::check(): Send, // <-- associated return type
78+
{
79+
tokio::task::spawn(async move {
80+
if !hc.check().await {
81+
log_health_check_failure().await;
82+
}
83+
});
84+
}
85+
```
86+
87+
Of course, it's kind of unfortunate that we had to rewrite from taking an `impl HealthCheck` to an explicit `HC` type parameter in order to use this notation. RFC #2289, "associated type bounds", introduced a compact notation to address this problem. That RFC is not part of this MVP, but if it were stabilized, then one could simply write:
88+
89+
```rust
90+
async fn do_health_check_par(hc: impl HealthCheck<check(): Send> + Send + 'static) {
91+
// -------------
92+
tokio::task::spawn(async move {
93+
if !hc.check().await {
94+
log_health_check_failure().await;
95+
}
96+
});
97+
}
98+
```
99+
100+
In our [previous post][pp], we [hypothesized](https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-in-trait-nightly.html#hypothesis-this-is-uncommon) that this problem might not occur often in practice. However, our case studies found that it comes up quite frequently, and so we decided that a solution is needed. We explored a number of solutions and concluded that associated return types are the most practical.
101+
102+
**Status:** Associated return types have an experimental implementation and we are currently drafting an RFC. There are several open bugs that will need to be fixed.
103+
104+
## MVP Part 3: "impl trait in traits" (return position)
105+
106+
All across Rust, an async function is "syntactic sugar" for a function that returns an `impl Future` -- and async functions in traits are no exception. As part of the MVP, we plan to stabilize the use of `-> impl Trait` notation in traits.
107+
108+
Impl trait in traits has all kinds of uses, but one common one for async programming is to avoid capturing all of the function arguments by doing some amount of sync work and then returning a future for the rest. For example, this `LaunchService` trait declares a `launch` function that does not capture `self` (similar to the existing Tower [`Service`] trait):
109+
110+
[`Service`]: https://docs.rs/tower/latest/tower/trait.Service.html
111+
112+
```rust
113+
trait LaunchService {
114+
fn launch(
115+
&mut self,
116+
request: Request,
117+
) -> impl Future<Output = u32>;
118+
// -------------------------
119+
// Does not capture `self` as it does
120+
// not include a `+ '_`.
121+
}
122+
```
123+
124+
Even though the need for "impl trait in traits" comes up a lot in async, they are a general feature that will be useful in many contexts having nothing to do with async (for example, returning iterators from trait methods).
125+
126+
**Status:** Return-position impl trait in traits have an experimental implementation and are described in the recently opened [RFC 3425].
127+
128+
[RFC 3425]: https://github.com/rust-lang/rfcs/pull/3425
129+
130+
## Evaluating the MVP
131+
132+
To evaluate the utility of this MVP, the working group collected [five case studies][] covering the [builder-provider pattern used in the AWS SDK](https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/builder-provider-api.html#dynamic-dispatch-behind-the-api); the potential use of async function in traits in [tower][cst] and the actual use in [embassy][cse], the [Fuchsia networking stack][] and [an internal Microsoft tool][]. These studies validated that the above functionality is sufficient to use async function in traits for all kinds of things, though some situations require workarounds (hence the "MVP" title).
133+
134+
[Fuchsia networking stack]: https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/socket-handler.html
135+
136+
[an internal Microsoft tool]: https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/microsoft.html
137+
138+
[cst]: https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/tower.html
139+
140+
[cse]: https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/embassy.html
141+
142+
[five case studies]: https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies.html
143+
144+
## What the MVP will not support or won't support well
145+
146+
The case studies revealed two situations that the MVP doesn't support very well, but both of them have workarounds available. These workarounds are mechanical and once the MVP is available on stable it will be possible to automate them via a custom derive or other crates on crates.io.
147+
148+
### Modeling dynamic dispatch
149+
150+
In the MVP, traits that use async functions are not "dyn safe", meaning that they don't support dynamic dispatch. So e.g. given the `HealthCheck` trait we saw earlier, one could not write `Box<dyn HealthCheck>`. At first, this seems like a crucial limitation, since many of the use cases require dynamic dispatch! But it turns out that there is a workaround. One can define an "erased" trait internally to your crate that enables dynamic dispatch. The process was pioneered by crates like [erased serde][] and is explained in detail in the [builder-provider case study][].
151+
152+
In the future, async fn should work with `dyn Trait` directly.
153+
154+
[erased serde]: https://github.com/dtolnay/erased-serde
155+
[builder-provider case study]: https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/builder-provider-api.html#dynamic-dispatch-behind-the-api
156+
157+
### Send bounds are verbose, especially for traits with lots of methods
158+
159+
The associated return type proposal works great for traits with a single method, but it can be annoying for traits that have lots of methods. One convenient solution is to use the "trait alias pattern" (if [RFC 1733](https://github.com/rust-lang/rust/issues/41517) were stabilized, this would be easier):
160+
161+
```rust
162+
trait SendHealthCheck
163+
where
164+
Self: HealthCheck + Send,
165+
Self::check(): Send,
166+
{}
167+
168+
impl<T> SendHealthCheck for T
169+
where
170+
T: HealthCheck + Send,
171+
T::check(): Send,
172+
{}
173+
```
174+
175+
Using a pattern like this means you can write `T: SendHealthCheck`. In the future, something like [trait transformers][] may provide a more concise syntax.
176+
177+
[trait transformers]: https://smallcultfollowing.com/babysteps/blog/2023/03/03/trait-transformers-send-bounds-part-3/
178+
179+
## Timeline and roadmap
180+
181+
Our goal is to stabilize the MVP for Rust 1.74, which will be released on 2023-11-16. The branch window for this feature opens on July 14 and closes on August 24. To actually stabilize in 1.74, we want to leave room for bug fixes that may arise before the release branch is cut. The key milestones for this goal are as follows:
182+
183+
* [x] MVP implementation
184+
* [x] Case study evaluations complete
185+
* [ ] Accepted RFC for return-position impl trait (target: 2023-05-31)
186+
* [ ] Accepted RFC for associated return types (target: 2023-06-15)
187+
* [ ] Evaluation period and bug fixing (target: 2023-06-30)
188+
* [ ] Stabilization report authored (target: 2023-07-01)
189+
* [ ] Stabilization complete for 1.74.0 (target: 2023-07-21)
190+
191+
You can find the [complete timeline in our github project][timeline].
192+
193+
[timeline]: https://github.com/orgs/rust-lang/projects/28/views/2
194+
195+
## What comes next?
196+
197+
So, once this MVP is done, what next? Our next immediate goals are to ship **dynamic dispatch** and **async closures** support in 2024. Together this will complete a solid foundation to tackle future async problems, such as support for async drop, easy async iterators, or portability across runtimes.

0 commit comments

Comments
 (0)