-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Implementable trait aliases #3437
Open
Jules-Bertholet
wants to merge
20
commits into
rust-lang:master
Choose a base branch
from
Jules-Bertholet:implementable-trait-alias
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
5ef6311
RFC: Implementable trait aliases
Jules-Bertholet af7b7ea
Fix wording
Jules-Bertholet 6b36635
Discuss `#[implementable]` in more detail
Jules-Bertholet 5aa3827
Add future possibility syntax
Jules-Bertholet cd871e0
Support fully-qualified method call syntax
Jules-Bertholet 5d1080a
Allow implementable aliases in paths more generally
Jules-Bertholet 13a9c18
Add discussion of "unioning" traits
Jules-Bertholet 1b32952
Fix typos
Jules-Bertholet f03d3b9
Expand alternatives, fix implementability rules hole
Jules-Bertholet d2603ab
Add words about associated type bounds
Jules-Bertholet 1e235ff
Fix typo
Jules-Bertholet 6c22c39
Address biggest comments from design meeting
Jules-Bertholet 149b016
Update motivation to reflect current state of AFIT
Jules-Bertholet 7089998
Correct minor error
Jules-Bertholet c08f2b8
Split one bullet into two
Jules-Bertholet d0c32f3
Alias associated items
Jules-Bertholet 1167e58
Clarify "weak"/"strong"
Jules-Bertholet b027df1
Future possibility: trait alias associated items
Jules-Bertholet 601cc9c
Future possibility: trait aliases constrained by their associated items
Jules-Bertholet 24f15c1
Rewrite AFIT example in terms of `trait-variant`
Jules-Bertholet 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 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,319 @@ | ||
- Feature Name: `trait_alias_impl` | ||
- Start Date: 2023-05-24 | ||
- RFC PR: [rust-lang/rfcs#3437](https://github.com/rust-lang/rfcs/pull/3437) | ||
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
||
# Summary | ||
|
||
Extend `#![feature(trait_alias)]` to permit `impl` blocks for trait aliases with a single primary trait. | ||
|
||
# Motivation | ||
|
||
Often, one desires to have a "weak" version of a trait, as well as a "strong" one providing additional guarantees. | ||
Subtrait relationships are commonly used for this, but they sometimes fall short—expecially when the "strong" version is | ||
expected to see more use, or was stabilized first. | ||
|
||
## Example: AFIT `Send` bound aliases | ||
|
||
### With subtraits | ||
|
||
Imagine a library, `frob-lib`, that provides a trait with an async method. (Think `tower::Service`.) | ||
|
||
```rust | ||
//! crate frob-lib | ||
pub trait Frobber { | ||
async fn frob(&self); | ||
} | ||
``` | ||
|
||
Most of `frob-lib`'s users will need `Frobber::frob`'s return type to be `Send`, | ||
so the library wants to make this common case as painless as possible. But non-`Send` | ||
usage should be supported as well. | ||
|
||
`frob-lib`, following the recommended practice, decides to design its API in the following way: | ||
|
||
```rust | ||
//! crate frob-lib | ||
|
||
pub trait LocalFrobber { | ||
async fn frob(&self); | ||
} | ||
|
||
// or whatever RTN syntax is decided on | ||
pub trait Frobber: LocalFrobber<frob(..): Send> + Send {} | ||
impl<T: ?Sized> Frobber for T where T: LocalFrobber<frob(..): Send> + Send {} | ||
``` | ||
|
||
These two traits are, in a sense, one trait with two forms: the "weak" `LocalFrobber`, and "strong" `Frobber` that offers an additional `Send` guarantee. | ||
|
||
Because `Frobber` (with `Send` bound) is the common case, `frob-lib`'s documentation and examples put it front and center. So naturally, Joe User tries to implement `Frobber` for his own type. | ||
|
||
```rust | ||
//! crate joes-crate | ||
use frob_lib::Frobber; | ||
|
||
struct MyType; | ||
|
||
impl Frobber for MyType { | ||
async fn frob(&self) { | ||
println!("Sloo is 120% klutzed. Initiating brop sequence...") | ||
} | ||
} | ||
``` | ||
|
||
But one `cargo check` later, Joe is greeted with: | ||
|
||
``` | ||
error[E0277]: the trait bound `MyType: LocalFrobber` is not satisfied | ||
--> src/lib.rs:6:18 | ||
| | ||
6 | impl Frobber for MyType { | ||
| ^^^^^^ the trait `LocalFrobber` is not implemented for `MyType` | ||
|
||
error[E0407]: method `frob` is not a member of trait `Frobber` | ||
--> src/lib.rs:7:5 | ||
| | ||
7 | / async fn frob(&self) { | ||
8 | | println!("Sloo is 120% klutzed. Initiating brop sequence...") | ||
9 | | } | ||
| |_____^ not a member of trait `Frobber` | ||
``` | ||
|
||
Joe is confused. "What's a `LocalFrobber`? Isn't that only for non-`Send` use cases? Why do I need to care about all that?" But he eventually figures it out: | ||
|
||
```rust | ||
//! crate joes-crate | ||
use frob_lib::LocalFrobber; | ||
|
||
struct MyType; | ||
|
||
impl LocalFrobber for MyType { | ||
#[refine] | ||
async fn frob(&self) { | ||
println!("Sloo is 120% klutzed. Initiating brop sequence...") | ||
} | ||
} | ||
``` | ||
|
||
This is distinctly worse. Joe now has to reference both `Frobber` and `LocalFrobber` in his code, | ||
and (assuming that the final AFIT feature ends up requiring it) also has to write `#[refine]`. | ||
|
||
### With today's `#![feature(trait_alias)]` | ||
|
||
What if `frob-lib` looked like this instead? | ||
|
||
```rust | ||
//! crate frob-lib | ||
#![feature(trait_alias)] | ||
|
||
pub trait LocalFrobber { | ||
async fn frob(&self); | ||
} | ||
|
||
pub trait Frobber = LocalFrobber<frob(..): Send> + Send; | ||
``` | ||
|
||
With today's `trait_alias`, it wouldn't make much difference for Joe. He would just get a slightly different | ||
error message: | ||
|
||
``` | ||
error[E0404]: expected trait, found trait alias `Frobber` | ||
--> src/lib.rs:6:6 | ||
| | ||
6 | impl Frobber for MyType { | ||
| ^^^^^^^ not a trait | ||
``` | ||
|
||
## Speculative example: GATification of `Iterator` | ||
|
||
*This example relies on some language features that are currently pure speculation. | ||
Implementable trait aliases are potentially necessary to support this use-case, but not sufficent.* | ||
|
||
Ever since the GAT MVP was stabilized, there has been discussion about how to add `LendingIterator` to the standard library, without breaking existing uses of `Iterator`. | ||
The relationship between `LendingIterator` and `Iterator` is "weak"/"strong"—an `Iterator` is a `LendingIterator` with some extra guarantees about the `Item` associated type. | ||
|
||
Now, let's imagine that Rust had some form of "variance bounds", | ||
that allowed restricting the way in which a type's GAT can depend on said GAT's generic parameters. | ||
One could then define `Iterator` in terms of `LendingIterator`, like so: | ||
|
||
```rust | ||
//! core::iter | ||
pub trait LendingIterator { | ||
type Item<'a> | ||
where | ||
Self: 'a; | ||
|
||
fn next(&'a mut self) -> Self::Item<'a>; | ||
} | ||
|
||
pub trait Iterator = LendingIterator | ||
where | ||
// speculative syntax, just for the sake of this example | ||
for<'a> Self::Item<'a>: bivariant_in<'a>; | ||
``` | ||
|
||
But, as with the previous example, we are foiled by the fact that trait aliases aren't `impl`ementable, | ||
so this change would break every `impl Iterator` block in existence. | ||
|
||
## Speculative example: `Async` trait | ||
|
||
There has been some discussion about a variant of the `Future` trait with an `unsafe` poll method, to support structured concurrency ([here](https://rust-lang.github.io/wg-async/vision/roadmap/scopes/capability/variant_async_trait.html) for example). *If* such a change ever happens, then the same "weak"/"strong" relationship will arise: the safe-to-poll `Future` trait would be a "strong" version of the unsafe-to-poll `Async`. As the linked design notes explain, there are major problems with expressing this relationship in today's Rust. | ||
|
||
# Guide-level explanation | ||
|
||
With `#![feature(trait_alias)]` (RFC #1733), one can define trait aliases, for use in bounds, trait objects, and `impl Trait`. This feature additionaly allows writing `impl` blocks for a subset of trait aliases. | ||
|
||
Let's rewrite our AFIT example from before, in terms of this feature. Here's what it looks like now: | ||
|
||
```rust | ||
//! crate frob-lib | ||
#![feature(trait_alias)] | ||
|
||
pub trait LocalFrobber { | ||
async fn frob(&self); | ||
} | ||
|
||
pub trait Frobber = LocalFrobber<frob(..): Send> | ||
where | ||
// not `+ Send`! | ||
Self: Send; | ||
``` | ||
|
||
```rust | ||
//! crate joes-crate | ||
#![feature(trait_alias_impl)] | ||
|
||
use frob_lib::Frobber; | ||
|
||
struct MyType; | ||
|
||
impl Frobber for MyType { | ||
async fn frob(&self) { | ||
println!("Sloo is 120% klutzed. Initiating brop sequence...") | ||
} | ||
} | ||
``` | ||
|
||
Joe's original code Just Works. | ||
|
||
The rule of thumb is: if you can copy everything between the `=` and `;` of a trait alias, and paste it between the | ||
`for` and `{` of a trait `impl` block, and the result is sytactically valid—then the trait alias is implementable. | ||
|
||
# Reference-level explanation | ||
|
||
A trait alias has the following syntax (using the Rust Reference's notation): | ||
|
||
> [Visibility](https://doc.rust-lang.org/stable/reference/visibility-and-privacy.html)<sup>?</sup> `trait` [IDENTIFIER](https://doc.rust-lang.org/stable/reference/identifiers.html) [GenericParams](https://doc.rust-lang.org/stable/reference/items/generics.html)<sup>?</sup> `=` [TypeParamBounds](https://doc.rust-lang.org/stable/reference/trait-bounds.html)<sup>?</sup> [WhereClause](https://doc.rust-lang.org/stable/reference/items/generics.html#where-clauses)<sup>?</sup> `;` | ||
|
||
For example, `trait Foo<T> = PartialEq<T> + Send where Self: Sync;` is a valid trait alias. | ||
|
||
Implementable trait aliases must follow a more restrictive form: | ||
|
||
> [Visibility](https://doc.rust-lang.org/stable/reference/visibility-and-privacy.html)<sup>?</sup> `trait` [IDENTIFIER](https://doc.rust-lang.org/stable/reference/identifiers.html) [GenericParams](https://doc.rust-lang.org/stable/reference/items/generics.html)<sup>?</sup> `=` [TypePath](https://doc.rust-lang.org/stable/reference/paths.html#paths-in-types) [WhereClause](https://doc.rust-lang.org/stable/reference/items/generics.html#where-clauses)<sup>?</sup> `;` | ||
|
||
For example, `trait Foo<T> = PartialEq<T> where Self: Sync;` is a valid implementable alias. The `=` must be followed by a single trait (or implementable trait alias), and then some number of where clauses. | ||
|
||
An impl block for a trait alias looks just like an impl block for the underlying trait. The alias's where clauses | ||
are treated as obligations that the the `impl`ing type must meet. | ||
|
||
```rust | ||
pub trait CopyIterator = Iterator<Item: Copy> where Self: Send; | ||
|
||
struct Foo; | ||
|
||
impl CopyIterator for Foo { | ||
type Item = i32; // Would be an error if this was `String` | ||
|
||
fn next(&mut self) -> Self::Item { | ||
42 | ||
} | ||
} | ||
|
||
struct Bar; | ||
impl !Send for Bar; | ||
|
||
// ERROR: `Bar` is not `Send` | ||
// impl IntIterator for Bar { /* ... */ } | ||
``` | ||
|
||
If the trait alias uniquely constrains a portion of the `impl` block, that part can be omitted. | ||
|
||
```rust | ||
pub trait IntIterator = Iterator<Item = i32> where Self: Send; | ||
|
||
struct Baz; | ||
|
||
impl IntIterator for Baz { | ||
// The alias constrains `Self::Item` to `i32`, so we don't need to specify it | ||
// (though we are allowed to do so if desired). | ||
// type Item = i32; | ||
|
||
fn next(&mut self) -> i32 { | ||
-27 | ||
} | ||
} | ||
``` | ||
|
||
Alias `impl`s also allow omitting implied `#[refine]`s: | ||
|
||
```rust | ||
//! crate frob-lib | ||
#![feature(trait_alias)] | ||
|
||
pub trait LocalFrobber { | ||
async fn frob(&self); | ||
} | ||
|
||
// not `+ Send`! | ||
pub trait Frobber = LocalFrobber<frob(..): Send> where Self: Send; | ||
``` | ||
|
||
```rust | ||
//! crate joes-crate | ||
#![feature(trait_alias_impl)] | ||
|
||
use frob_lib::Frobber; | ||
|
||
struct MyType; | ||
|
||
impl Frobber for MyType { | ||
// The return future of this method is implicitly `Send`, as implied by the alias. | ||
// No `#[refine]` is necessary. | ||
async fn frob(&self) { | ||
println!("Sloo is 120% klutzed. Initiating brop sequence...") | ||
} | ||
} | ||
``` | ||
|
||
Trait aliases are `unsafe` to implement iff the underlying trait is marked `unsafe`. | ||
|
||
# Drawbacks | ||
|
||
- The sytactic distance between implementable and non-implementable aliases is short, which might confuse users. | ||
In particular, the fact that `trait Foo = Bar + Send;` means something different than `trait Foo = Bar where Self: Send;` will likely be surprising to many. | ||
- Adds complexity to the language. | ||
- Many of the motivating use-cases involve language features that are not yet stable, or even merely speculative. | ||
More experience with those features might unearth better alternatives. | ||
|
||
# Rationale and alternatives | ||
|
||
- Very lightweight, with no new syntax forms. Compare "trait transformers" proposals, for example—they are generally much heavier. | ||
- Better ergonomics compared to purely proc-macro based solutions. | ||
- One alternative is to allow marker traits or auto traits to appear in `+` bounds of implementable aliases. | ||
(For example, `trait Foo = Bar + Send;` could be made implementable). However, I suspect that the complexity would not be worthwhile. | ||
- Another possibility is to require an attribute on implmenentable aliase; e.g. `#[implementable] trait Foo = ...`. Again, I don't think that the complexity is warranted. | ||
|
||
# Prior art | ||
|
||
- [`trait_transformer` macro](https://github.com/google/impl_trait_utils) | ||
|
||
# Unresolved questions | ||
|
||
- How does `rustdoc` render these? Consider the `Frobber` example—ideally, `Frobber` should be emphasized | ||
compared to `LocalFrobber`, but it's not clear how that would work. | ||
|
||
# Future possibilities | ||
|
||
- New kinds of bounds: anything that makes `where` clauses more powerful would make this feature more powerful as well. | ||
- Variance bounds would allow this feature to support backward-compatible GATification. | ||
- Method unsafety bounds would support the `Future` → `Async` use-case. |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one benefit of requiring
#[implementable]
is that it mitigates the confusing aspect of:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added some more discussion about this.