-
Notifications
You must be signed in to change notification settings - Fork 71
Add "Evolving trait hierarchies" project proposal #353
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 all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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,266 @@ | ||
# Evolving trait hierarchies | ||
|
||
| Metadata | | | ||
|:-----------------|----------------------------------------------------------------------------------| | ||
| Point of contact | @cramertj | | ||
| Teams | <!-- TEAMS WITH ASKS --> | | ||
| Task owners | <!-- TASK OWNERS --> | | ||
| Status | Proposed | | ||
| Tracking issue | | | ||
| Zulip channel | | | ||
|
||
## Summary | ||
|
||
Unblock the evolution of key trait hierarchies: | ||
|
||
* Adding [`Receiver`](https://doc.rust-lang.org/std/ops/trait.Receiver.html) | ||
as a supertrait of [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html). | ||
* Allow the `tower::Service` trait to be split into a non-`Sync` supertrait and a | ||
`Sync` (thread-safe) subtrait. | ||
|
||
The design should incorporate the feedback from the | ||
[Evolving trait hierarchies](https://hackmd.io/6JId0y8LTyCzVMfZFimPqg) | ||
language design meeting. The design should also set the stage for future changes to allow for | ||
the more general case of splitting items out into supertraits. | ||
|
||
## Motivation | ||
|
||
Two significant motivating cases are discussed in [RFC 3437 "Implementable trait alias"](https://github.com/Jules-Bertholet/rfcs/blob/implementable-trait-alias/text/3437-implementable-trait-alias.md#deref--receiver--deref) under the heading "splitting a trait". | ||
|
||
### Conceptual supertrait: `Deref: Receiver` | ||
|
||
The `Deref` trait currently looks like this: | ||
|
||
```rust | ||
pub trait Deref { | ||
type Target: ?Sized; | ||
|
||
fn deref(&self) -> &Self::Target; | ||
} | ||
``` | ||
|
||
More recently, the `arbitrary_self_types` feature has motivated a more general `Receiver` trait: | ||
|
||
```rust | ||
pub trait Receiver { | ||
type Target: ?Sized; | ||
} | ||
``` | ||
|
||
Ideally, `Reciever` would be a supertrait of `Deref`: | ||
|
||
```rust | ||
pub trait Receiver { | ||
type Target: ?Sized; | ||
} | ||
|
||
pub trait Deref: Receiver { | ||
fn deref(&self) -> &Self::Target; | ||
} | ||
``` | ||
|
||
but this cannot be done today without a breaking change. | ||
|
||
More details are in this Pre-RFC: [Supertrait associated items in subtrait impl](https://hackmd.io/@rust-for-linux-/SkucBLsWxl) | ||
|
||
### Conceptual supertrait: `Iterator: LendingIterator` | ||
|
||
Similarly, every type that implements today's `Iterator` trait could also (conceptually) be a `LendingIterator`: | ||
|
||
```rust | ||
pub trait LendingIterator { | ||
type Item<'a> where Self: 'a; | ||
fn next(&mut self) -> Option<Self::Item<'_>>; | ||
} | ||
|
||
pub trait Iterator { | ||
type Item; | ||
fn next(&mut self) -> Option<Self::Item>; | ||
} | ||
``` | ||
|
||
#### Missing or misnamed parent trait items | ||
|
||
Note that, unlike `Deref` and `Receiver`, the names and signatures of the associated items in `LendingIterator` do not match those in `Iterator`. For existing `Iterator` types to implement `LendingIterator`, some bridge code between the two implementations must exist. | ||
|
||
|
||
### Conceptual supertrait: relaxed bounds | ||
|
||
A common practice in the `async` world is to use [the `trait_variant` crate](https://docs.rs/trait-variant/latest/trait_variant/) to make two versions of a trait, one with `Send` bounds on the futures, and one without: | ||
|
||
```rust | ||
#[trait_variant::make(IntFactory: Send)] | ||
trait LocalIntFactory { | ||
async fn make(&self) -> i32; | ||
fn stream(&self) -> impl Iterator<Item = i32>; | ||
fn call(&self) -> u32; | ||
} | ||
|
||
// `trait_variant` will generate a conceptual subtrait: | ||
|
||
trait IntFactory: Send { | ||
fn make(&self) -> impl Future<Output = i32> + Send; | ||
fn stream(&self) -> impl Iterator<Item = i32> + Send; | ||
fn call(&self) -> u32; | ||
} | ||
``` | ||
|
||
In this example, a type that implements `IntFactory` also satisfies the requirements for `LocalIntFactory`, but additionally guarantees that the returned types are `Send`. | ||
|
||
### The status quo | ||
|
||
Today's solutions include: | ||
|
||
### Add a supertrait | ||
|
||
#### Pros | ||
* Matches the conceptual pattern between the traits: one clearly implies the other. | ||
* The "standard" Rust way of doing things. | ||
|
||
#### Cons | ||
* Breaking change. | ||
* Requires implementors to split out the implementation into separate `impl` blocks for each trait. | ||
|
||
### Add a blanket impl | ||
|
||
Rather than declaring `trait A: B`, one can create sibling traits with a blanket impl: | ||
|
||
```rust | ||
trait Supertrait { | ||
// supertrait items | ||
} | ||
|
||
trait Subtrait { | ||
// supertrait items + subtrait items | ||
} | ||
|
||
impl<T: Subtrait> Supertrait for T { | ||
// impl supertrait items using subtrait items | ||
} | ||
``` | ||
|
||
#### Pros | ||
* Backwards compatible to introduce `Supertrait` | ||
|
||
#### Cons | ||
|
||
* Middleware impls are impossible! We'd like to write: | ||
|
||
```rust | ||
struct Middeware<T>(T); | ||
|
||
impl<T: Supertrait> Supertrait for Middleware<T> { ... } | ||
|
||
impl<T: Subtrait> Subtrait for Middleware<T> { ... } | ||
``` | ||
|
||
but this overlaps with the blanket impl, and is rejected by the Rust compiler! This is a critical issue for `async` bridge APIs such as tower's `Service` trait, which wants to provide wrappers which implement the `trait_variant`-style `Send`-able when the underlying type implements the `Send` version (see [these notes from a previous design meeting](https://hackmd.io/rmN25qziSHKT4kv-ZC8QPw)). | ||
|
||
Other nits: | ||
* The directionality of the impl is less clear. | ||
* Every shared item has two names: `<T as Supertrait>::Item` and `<T as Subtrait>::Item`. Relatedly, the bridge impl must exist even if items are identical. | ||
|
||
### Provide no bridging | ||
|
||
Another alternative is to provide two totally separate traits with no bridging, requiring users to manually implement both versions of the trait: | ||
|
||
```rust | ||
trait Supertrait { ... } | ||
trait Subtrait { ... } | ||
|
||
struct Impl { ... } | ||
|
||
impl Supertrait for T { ... } | ||
impl Subtrait for T { ... } | ||
``` | ||
|
||
#### Pros | ||
|
||
* Backwards compatible | ||
* Middleware can be written to bridge either impl | ||
|
||
#### Cons | ||
|
||
* Requires duplication | ||
* Requires users to restate bounds | ||
* Existing code which implements `Subtrait` cannot be used as `Supertrait`, so APIs which require `Subtrait` cannot be relaxed to `Supertrait` | ||
|
||
### The next 6 months | ||
|
||
In the next six months, we aim to ship a solution which addresses the `Service` and `Receiver` use-cases | ||
by allowing trait impls to implement supertraits *if* the impl itself contains a definition of an item | ||
from the supertrait. | ||
|
||
Traits will have to opt their impls into this behavior, possibly through the use of a keyword. | ||
`auto` is used below as an example keyword: | ||
|
||
```rust | ||
// Library code: | ||
trait Subtrait { | ||
fn supertrait_item(); | ||
fn subtrait_item(); | ||
} | ||
// User code: | ||
impl Subtrait for MyType { | ||
fn supertrait_item() { ... } | ||
fn subtrait_item() { ... } | ||
} | ||
|
||
// -- can become -- | ||
|
||
// Library code: | ||
trait Supertrait { | ||
fn supertrait_item(); | ||
} | ||
trait Subtrait: Supertrait { | ||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fn subtrait_item(); | ||
} | ||
// User code is unchanged from above, no separate `Supertrait` | ||
// impl required | ||
impl Subtrait for MyType { | ||
fn supertrait_item() { ... } | ||
fn subtrait_item() { ... } | ||
} | ||
``` | ||
|
||
### The "shiny future" we are working towards | ||
|
||
In the future, we'd like it to be backwards-compatible for traits to split out arbitrary, possibly | ||
defaulted items into supertraits. | ||
|
||
This will be challenging, as it won't be obvious syntactically whether an impl intends to provide a | ||
supertrait impl-- some degree of coherence / overlap resolution will be required. However, this feature | ||
will provide library authors a great deal of flexibility while allowing for more ergonomic end-user | ||
implementations. | ||
|
||
## Ownership and team asks | ||
|
||
| Task | Owner(s) or team(s) | Notes | | ||
|------------------------------------------|----------------------|----------------------------------| | ||
| Discussion and moral support | ![Team][] [lang] | | | ||
| Lang-team champion | @cramertj | | | ||
| Discussion and moral support | ![Team][] [compiler] | | | ||
| Discussion and moral support | ![Team][] [types] | | | ||
| Author RFC | @cramertj | | | ||
| Implementation | @cramertj & others | | | ||
| Stabilization decision | ![Team][] [libs-api] | Stabilizing `Receiver`. Unblocked by implementation. | | ||
| Stabilization decision | ![Team][] [lang] | Stabilizing `arbitrary_self_types`. Unblocked by new `Receiver` API. | | ||
|
||
### Definitions | ||
|
||
For definitions for terms used above, see the [About > Team Asks](https://rust-lang.github.io/rust-project-goals/about/team_asks.html) page. | ||
|
||
* *Discussion and moral support* is the lowest level offering, basically committing the team to nothing but good vibes and general support for this endeavor. | ||
* *Author RFC* and *Implementation* means actually writing the code, document, whatever. | ||
* *Design meeting* means holding a synchronous meeting to review a proposal and provide feedback (no decision expected). | ||
* *RFC decisions* means reviewing an RFC and deciding whether to accept. | ||
* *Org decisions* means reaching a decision on an organizational or policy matter. | ||
* *Secondary review* of an RFC means that the team is "tangentially" involved in the RFC and should be expected to briefly review. | ||
* *Stabilizations* means reviewing a stabilization and report and deciding whether to stabilize. | ||
* *Standard reviews* refers to reviews for PRs against the repository; these PRs are not expected to be unduly large or complicated. | ||
* *Prioritized nominations* refers to prioritized lang-team response to nominated issues, with the expectation that there will be *some* response from the next weekly triage meeting. | ||
* *Dedicated review* means identifying an individual (or group of individuals) who will review the changes, as they're expected to require significant context. | ||
* Other kinds of decisions: | ||
* [Lang team experiments](https://lang-team.rust-lang.org/how_to/experiment.html) are used to add nightly features that do not yet have an RFC. They are limited to trusted contributors and are used to resolve design details such that an RFC can be written. | ||
* Compiler [Major Change Proposal (MCP)](https://forge.rust-lang.org/compiler/mcp.html) is used to propose a 'larger than average' change and get feedback from the compiler team. | ||
* Library [API Change Proposal (ACP)](https://std-dev-guide.rust-lang.org/development/feature-lifecycle.html) describes a change to the standard library. |
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.