You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: posts/inside-rust/2022-11-17-async-fn-in-trait-nightly.md
+58-41Lines changed: 58 additions & 41 deletions
Original file line number
Diff line number
Diff line change
@@ -5,35 +5,47 @@ author: Tyler Mandry
5
5
team: The Rust Async Working Group <https://www.rust-lang.org/governance/wgs/wg-async>
6
6
---
7
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.
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
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.
10
+
```rust
11
+
#![feature(async_fn_in_trait)]
12
+
13
+
traitDatabase {
14
+
asyncfnfetch_data(&self) ->String;
15
+
}
16
+
17
+
implDatabaseforMyDb {
18
+
asyncfnfetch_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.
11
23
12
-
## Recap: The problem of `async fn` in trait
24
+
## Recap: How async/await works in Rust
13
25
14
-
An`async fn` returns a `Future`, which is some object that represents an ongoing asynchronous computation.
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.
15
27
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:
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:
17
29
18
30
```rust
19
-
implMyDatabase {
20
-
asyncfnfetch_data(&self) ->String { ... }
21
-
}
31
+
asyncfnfetch_data(db:&MyDb) ->String { ... }
22
32
```
23
33
24
34
The compiler rewrites it to something like this:
25
35
26
36
```rust
27
-
implMyDatabase {
28
-
fnfetch_data<'a>(&'aself) ->implFuture<Output=String> + 'a {
29
-
asyncmove { ... }
30
-
}
37
+
fnfetch_data<'a>(db:&'aMyDb) ->implFuture<Output=String> + 'a {
38
+
asyncmove { ... }
31
39
}
32
40
```
33
41
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*.
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
35
47
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:
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:
37
49
38
50
```rust
39
51
traitDatabase {
@@ -42,26 +54,26 @@ trait Database {
42
54
}
43
55
```
44
56
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`:
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`:
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]
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]
55
67
56
68
[^tait]: This feature is called ["type alias impl trait"](https://rust-lang.github.io/rfcs/2515-type_alias_impl_trait.html).
57
69
58
-
### Workarounds available today
70
+
### Workarounds available in the stable compiler
59
71
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.
72
+
So to write an `async fn` in a trait we need a concrete type to specify in our impl, if not our trait. There are a couple ways of achieving this today.
61
73
62
74
#### Runtime type erasure
63
75
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:
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:
65
77
66
78
```rust
67
79
traitDatabase {
@@ -70,7 +82,7 @@ trait Database {
70
82
}
71
83
```
72
84
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
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
74
86
75
87
```rust
76
88
#[async_trait]
@@ -79,18 +91,24 @@ trait Database {
79
91
}
80
92
81
93
#[async_trait]
82
-
implDatabaseforMyDatabase {
94
+
implDatabaseforMyDb {
83
95
asyncfnfetch_data(&self) ->String { ... }
84
96
}
85
97
```
86
98
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.
99
+
This is an excellent solution for the people who can use it!
88
100
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.
101
+
Unfortunately, not everyone can. You can't use `Box` 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.
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.
90
104
91
105
#### Manual `poll` implementations
92
106
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:
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 Stream[^stream-futures] trait. The signature of `Stream::poll_next` is a cross between `Future::poll` and `Iterator::next`.
110
+
111
+
[^stream-futures]: The [`Stream` trait](https://docs.rs/futures/latest/futures/stream/trait.Stream.html) is part of the `futures` crate, not the standard library.
94
112
95
113
```rust
96
114
pubtraitStream {
@@ -103,13 +121,11 @@ pub trait Stream {
103
121
}
104
122
```
105
123
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
124
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
125
110
126
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
127
112
-
## What's new
128
+
## What's supported in nightly
113
129
114
130
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
131
@@ -122,7 +138,7 @@ trait Database {
122
138
asyncfnfetch_data(&self) ->String;
123
139
}
124
140
125
-
implDatabaseforMyDatabase {
141
+
implDatabaseforMyDb {
126
142
asyncfnfetch_data(&self) ->String { ... }
127
143
}
128
144
```
@@ -138,7 +154,7 @@ trait AsyncIterator {
138
154
}
139
155
```
140
156
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.
157
+
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 write generic code with it, just like you would normally.
142
158
143
159
```rust
144
160
asyncfnprint_all<I:AsyncIterator>(mutcount:I)
@@ -153,7 +169,7 @@ where
153
169
154
170
### Limitation: Spawning from generics
155
171
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]
172
+
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]
[^actual-error]: The actual error message produced by the compiler is a bit noisier than this, but that can be improved.
176
192
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.
193
+
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.
178
194
179
195
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.
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]
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]
194
211
195
212
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
213
@@ -200,7 +217,7 @@ We would like to start gathering data on people's actual experiences with this.
200
217
201
218
Eventually you probably *will* want to spawn from a context that's generic over an async trait that you call. What then!?
202
219
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:
220
+
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:
204
221
205
222
```rust
206
223
#![feature(return_position_impl_trait_in_trait)]
@@ -219,7 +236,7 @@ This solution is intended to be temporary. We'd like to implement a better solut
219
236
220
237
### Limitation: Dynamic dispatch
221
238
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.
239
+
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.
223
240
224
241
## Path to stabilization
225
242
@@ -230,7 +247,7 @@ The async working group would like to get something useful in the hands of Rust
230
247
There are two big questions to answer first:
231
248
232
249
***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).
250
+
***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).
234
251
235
252
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
253
@@ -249,15 +266,15 @@ This work was made possible thanks to the efforts of many people, including
249
266
* Niko Matsakis
250
267
* Tyler Mandry
251
268
252
-
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. Thanks to all those who contributed.
269
+
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. I'm deeply grateful to all those who contributed; this work would not be possible without you. Thank you!
253
270
254
271
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.
255
272
256
273
257
-
_Thanks to Yoshua Wuyts, Dan Johnson, Santiago Pastorino, and Eric Holk for reviewing a draft of this post._
274
+
_Thanks to Yoshua Wuyts, Nick Cameron, Dan Johnson, Santiago Pastorino, Eric Holk, and Niko Matsakis for reviewing a draft of this post._
258
275
259
276
260
-
[^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.
277
+
[^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.
261
278
[^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/).
0 commit comments