-
Notifications
You must be signed in to change notification settings - Fork 303
Announce async fn
and return-position impl Trait
in traits
#1180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a768e78
Announce `async fn` and return-position `impl Trait` in traits
tmandry eb3bc78
Apply suggestions from code review
tmandry 8e09cef
Apply feedback from code review
tmandry 0ea8859
Clarify use of RPIT(IT) and its limitations
tmandry cfca89e
Add paragraph about future roadmap goals
tmandry af2b731
Clarify that new features can build on top of `async fn` in traits
tmandry 2881cb3
Apply suggestions from code review
tmandry 1240a04
Remove footnote on desugaring
tmandry d6ff16c
Only one link to the crate/docs is necessary
tmandry 3b5dbad
Move blog post reference to FAQ
tmandry File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
--- | ||
layout: post | ||
title: "Announcing `async fn` and return-position `impl Trait` in traits" | ||
author: Tyler Mandry | ||
team: The Async Working Group <https://www.rust-lang.org/governance/wgs/wg-async> | ||
--- | ||
|
||
The Rust Async Working Group is excited to announce major progress towards our goal of enabling the use of `async fn` in traits. Rust 1.75, which hits stable next week, will include support for both `-> impl Trait` notation and `async fn` in traits. | ||
|
||
This is a big milestone, and we know many users will be itching to try these out in their own code. However, we are still missing some important features that many users need. Read on for an recommendations on when and how to use the stabilized features. | ||
|
||
## What's stabilizing | ||
|
||
Ever since [RFC #1522], Rust has allowed users to write `impl Trait` as the return type of functions. This means that the function returns "some type that implements `Trait`". This is commonly used to return iterators, closures, or other types that are complex or even impossible to write explicitly: | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[RFC #1522]: https://rust-lang.github.io/rfcs/1522-conservative-impl-trait.html | ||
|
||
```rust | ||
/// Given a list of players, return an iterator | ||
/// over their names. | ||
fn player_names( | ||
players: &[Player] | ||
) -> impl Iterator<Item = &String> { | ||
players | ||
.iter() | ||
.map(|p| &p.name) | ||
} | ||
``` | ||
|
||
Starting in Rust 1.75, you can use **return-position `impl Trait` in trait** (RPITIT) definitions and in trait impls. For example, you could use this to write a trait that returns an iterator: | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
trait Container { | ||
fn items(&self) -> impl Iterator<Item = Widget>; | ||
} | ||
|
||
impl Container for MyContainer { | ||
fn items(&self) -> impl Iterator<Item = Widget> { | ||
self.items.iter().cloned() | ||
} | ||
} | ||
``` | ||
|
||
So what does all of this have to do with async functions? Well, async functions are "just sugar" for a function that returns `-> impl Future`. Since that is now permitted in traits, **we also permit you to write traits that use `async fn`**.[^desugar-correct] | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
trait HttpService { | ||
async fn fetch(&self, url: Url) -> HtmlBody; | ||
// ^^^^^^^^ desugars to: | ||
// fn fetch(&self, url: Url) -> impl Future<Output = HtmlBody>; | ||
} | ||
``` | ||
|
||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[^desugar-correct]: See the FAQ on `impl Future + '_` for why this desugaring is correct. | ||
|
||
## Where the gaps lie | ||
|
||
### `-> impl Trait` in public traits | ||
|
||
The use of `-> impl Trait` is still discouraged for general use in **public** traits, for the reason that users can't use additional bounds on the return type. For example, there is no way to write this function: | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
fn print_in_reverse(container: impl Container) { | ||
for item in container.items().rev() { | ||
// ERROR: ^^^ | ||
// the trait `DoubleEndedIterator` | ||
// is not implemented for | ||
// `impl Iterator<Item = Widget>` | ||
eprintln!("{item}"); | ||
} | ||
} | ||
``` | ||
|
||
In the future we plan to add a solution for this. For now, this feature is best used in internal traits or when you're confident your users won't need additional bounds like this. Otherwise, you should continue using associated types where possible (note that associated types cannot yet be used when returning types that cannot be named). | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### `async fn` in public traits | ||
|
||
Since `async fn` desugars to `-> impl Future`, the same limitations apply. If you use bare `async fn` in a public trait today, you'll see a warning. | ||
|
||
``` | ||
warning: use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified | ||
--> src/lib.rs:7:5 | ||
| | ||
7 | async fn fetch(&self, url: Url) -> HtmlBody; | ||
| ^^^^^ | ||
| | ||
help: you can desugar to a normal `fn` that returns `impl Future` and add any desired bounds such as `Send`, but these cannot be relaxed without a breaking API change | ||
| | ||
7 - async fn fetch(&self, url: Url) -> HtmlBody; | ||
7 + fn fetch(&self, url: Url) -> impl std::future::Future<Output = HtmlBody> + Send; | ||
| | ||
``` | ||
|
||
Of particular interest to users of async are `Send` bounds on the returned future. Since users cannot add bounds later, the error message is saying that you as a trait author need to make a choice: Do you want your trait to work with multithreaded, work-stealing executors? | ||
|
||
Thankfully, we have a solution that allows using `async fn` in public traits today! We recommend using the [`trait_variant::make`](https://docs.rs/trait-variant/latest/trait_variant/attr.make.html) proc macro to let your users choose. This proc macro is part of the [`trait-variant`](https://crates.io/crates/trait-variant), published by the rust-lang org. Add it to your project with `cargo add trait-variant`, then use it like so: | ||
|
||
```rust | ||
#[trait_variant::make(HttpService: Send)] | ||
pub trait LocalHttpService { | ||
async fn fetch(&self, url: Url) -> HtmlBody; | ||
} | ||
``` | ||
|
||
This creates *two* versions of your trait: `LocalHttpService` for single-threaded executors and `HttpService` for multithreaded work-stealing executors. Since we expect the latter to be used more commonly, it has the shorter name in this example. It has additional Send bounds: | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
pub trait HttpService: Send { | ||
fn fetch( | ||
&self, | ||
url: Url, | ||
) -> impl Future<Output = HtmlBody> + Send; | ||
} | ||
``` | ||
|
||
This macro works for async because `impl Future` rarely requires additional bounds other than Send, so we can set our users up for success. See [this blog post](https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html) for a more thorough explanation of the problem.[^cut-scope] | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[^cut-scope]: Note that we originally said we would solve the Send bound problem before shipping `async fn` in traits, but we decided to cut that from the scope and ship the `trait-variant` crate instead. | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Dynamic dispatch | ||
|
||
Traits that use `-> impl Trait` and `async fn` are not object-safe, which means they lack support for dynamic dispatch. We plan to provide utilities that enable dynamic dispatch in an upcoming version of the `trait-variant` crate. | ||
|
||
## What we hope to do in the future | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
In the future we would like to allow users to add their own bounds to `impl Trait` return types, which would make them more generally useful. It would also enable more advanced uses of `async fn`. The syntax might look something like this: | ||
|
||
```rust | ||
trait HttpService = LocalHttpService<fetch(): Send> + Send; | ||
``` | ||
|
||
Since these aliases won't require any support on the part of the trait author, it will technically make the `Send` variants of async traits unnecessary. However, those variants will still be a nice convenience for users, so we expect that most crates will continue to provide them. | ||
|
||
## Frequently asked questions | ||
|
||
### Is it okay to use `-> impl Trait` in traits? | ||
|
||
For private traits you can use `-> impl Trait` freely. For public traits, it's best to avoid them for now unless you can anticipate all the bounds your users might want (in which case you can use `#[trait_variant::make]`, as we do for async). We expect to lift this restriction in the future. | ||
|
||
### Should I still use the `#[async_trait]` macro? | ||
|
||
There are a couple of reasons you might need to continue using async-trait: | ||
|
||
* You want to support Rust versions older than 1.75. | ||
* You want dynamic dispatch. | ||
|
||
As stated above, we hope to enable dynamic dispatch in a future version of the `trait-variant` crate. | ||
|
||
### Is it okay to use `async fn` in traits? What are the limitations? | ||
|
||
Assuming you don't need to use `#[async_trait]` for one of the reasons stated above, it's totally fine to use regular `async fn` in traits. Just remember to use `#[trait_variant::make]` if you want to support multithreaded runtimes. | ||
|
||
The biggest limitation is that a type must always decide if it implements the Send or non-Send version of a trait. It cannot implement the Send version *conditionally* on one of its generics. This can come up in the [middleware](https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md) pattern, for example, `RequestLimitingService<T>` that is HttpService if `T: HttpService`. | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Why do I need `#[trait_variant::make]` and `Send` bounds? | ||
|
||
In simple cases you may find that your trait appears to work fine with a multithreaded executor. There are some patterns that just won't work, however. Consider the following: | ||
|
||
```rust | ||
fn spawn_task(service: impl HttpService + 'static) { | ||
tokio::spawn(async move { | ||
let url = Url::from("https://rust-lang.org"); | ||
let _body = service.fetch(url).await; | ||
}); | ||
} | ||
``` | ||
|
||
Without Send bounds on our trait, this would fail to compile with the error: "future cannot be sent between threads safely". By creating a variant of your trait with Send bounds, you avoid sending your users into this trap. | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Note that you won't see a warning if your trait is not public, because if you run into this problem you can always add the Send bounds yourself later. | ||
|
||
### Can I mix async fn and impl trait? | ||
|
||
Yes, you can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound.[^leakage] This makes the traits created by `trait_variant` nicer to use. | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
trait HttpService: Send { | ||
fn fetch(&self, url: Url) | ||
-> impl Future<Output = HtmlBody> + Send; | ||
} | ||
|
||
impl HttpService for MyService { | ||
async fn fetch(&self, url: Url) -> HtmlBody { | ||
// This works, as long as `do_fetch(): Send`! | ||
self.client.do_fetch(url).await.into_body() | ||
} | ||
} | ||
``` | ||
|
||
[^leakage]: This works because of *auto-trait leakage*, which allows knowledge of auto traits to "leak" from an item whose signature does not specify them. | ||
|
||
### Why don't these signatures use `impl Future + '_`? | ||
|
||
For `-> impl Trait` in traits we adopted the [2024 Capture Rules] early. This means that the `+ '_` you often see today is unnecessary in traits, because the return type is already assumed to capture input lifetimes. In the 2024 edition this rule will apply to all function signatures. See the linked RFC for more. | ||
|
||
[2024 Capture Rules]: https://rust-lang.github.io/rfcs/3498-lifetime-capture-rules-2024.html | ||
|
||
### Why am I getting a "refine" warning when I implement a trait with `-> impl Trait`? | ||
|
||
If your impl signature includes more detailed information than the trait itself, you'll [get a warning](https://play.rust-lang.org/?version=beta&mode=debug&edition=2021&gist=6248cfe0419a693d1331ef47c729d1fe): | ||
|
||
```rust | ||
pub trait Foo { | ||
fn foo(self) -> impl Debug; | ||
} | ||
|
||
impl Foo for u32 { | ||
fn foo(self) -> String { | ||
// ^^^^^^ | ||
// warning: impl trait in impl method signature does not match trait method signature | ||
self.to_string() | ||
} | ||
} | ||
``` | ||
|
||
The reason is that you may be leaking more details of your implementation than you meant to. For instance, should the following code compile? | ||
|
||
```rust | ||
fn main() { | ||
// Did the implementer mean to allow | ||
// use of `Display`, or only `Debug` as | ||
// the trait says? | ||
println!("{}", 32.foo()); | ||
} | ||
``` | ||
|
||
Thanks to [refined trait implementations][3245] it does compile, but the compiler asks you to confirm your intent to refine the trait interface with `#[allow(refining_impl_trait)]` on the impl. | ||
|
||
[3245]: https://rust-lang.github.io/rfcs/3245-refined-impls.html | ||
|
||
## Conclusion | ||
|
||
The Async Working Group is excited to end 2023 by announcing the completion of our primary goal for the year! Thank you to everyone who helpfully participated in design, implementation, and stabilization discussions. Thanks also to the users of async Rust who have given great feedback over the years. We're looking forward to seeing what you build, and to delivering continued improvements in the years to come. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.