Skip to content

Commit b228648

Browse files
authored
Add "Evolving trait hierarchies" project proposal (#353)
1 parent 7099f34 commit b228648

File tree

1 file changed

+266
-0
lines changed

1 file changed

+266
-0
lines changed

src/2025h2/evolving-traits.md

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
# Evolving trait hierarchies
2+
3+
| Metadata | |
4+
|:-----------------|----------------------------------------------------------------------------------|
5+
| Point of contact | @cramertj |
6+
| Teams | <!-- TEAMS WITH ASKS --> |
7+
| Task owners | <!-- TASK OWNERS --> |
8+
| Status | Proposed |
9+
| Tracking issue | |
10+
| Zulip channel | |
11+
12+
## Summary
13+
14+
Unblock the evolution of key trait hierarchies:
15+
16+
* Adding [`Receiver`](https://doc.rust-lang.org/std/ops/trait.Receiver.html)
17+
as a supertrait of [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html).
18+
* Allow the `tower::Service` trait to be split into a non-`Sync` supertrait and a
19+
`Sync` (thread-safe) subtrait.
20+
21+
The design should incorporate the feedback from the
22+
[Evolving trait hierarchies](https://hackmd.io/6JId0y8LTyCzVMfZFimPqg)
23+
language design meeting. The design should also set the stage for future changes to allow for
24+
the more general case of splitting items out into supertraits.
25+
26+
## Motivation
27+
28+
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".
29+
30+
### Conceptual supertrait: `Deref: Receiver`
31+
32+
The `Deref` trait currently looks like this:
33+
34+
```rust
35+
pub trait Deref {
36+
type Target: ?Sized;
37+
38+
fn deref(&self) -> &Self::Target;
39+
}
40+
```
41+
42+
More recently, the `arbitrary_self_types` feature has motivated a more general `Receiver` trait:
43+
44+
```rust
45+
pub trait Receiver {
46+
type Target: ?Sized;
47+
}
48+
```
49+
50+
Ideally, `Reciever` would be a supertrait of `Deref`:
51+
52+
```rust
53+
pub trait Receiver {
54+
type Target: ?Sized;
55+
}
56+
57+
pub trait Deref: Receiver {
58+
fn deref(&self) -> &Self::Target;
59+
}
60+
```
61+
62+
but this cannot be done today without a breaking change.
63+
64+
More details are in this Pre-RFC: [Supertrait associated items in subtrait impl](https://hackmd.io/@rust-for-linux-/SkucBLsWxl)
65+
66+
### Conceptual supertrait: `Iterator: LendingIterator`
67+
68+
Similarly, every type that implements today's `Iterator` trait could also (conceptually) be a `LendingIterator`:
69+
70+
```rust
71+
pub trait LendingIterator {
72+
type Item<'a> where Self: 'a;
73+
fn next(&mut self) -> Option<Self::Item<'_>>;
74+
}
75+
76+
pub trait Iterator {
77+
type Item;
78+
fn next(&mut self) -> Option<Self::Item>;
79+
}
80+
```
81+
82+
#### Missing or misnamed parent trait items
83+
84+
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.
85+
86+
87+
### Conceptual supertrait: relaxed bounds
88+
89+
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:
90+
91+
```rust
92+
#[trait_variant::make(IntFactory: Send)]
93+
trait LocalIntFactory {
94+
async fn make(&self) -> i32;
95+
fn stream(&self) -> impl Iterator<Item = i32>;
96+
fn call(&self) -> u32;
97+
}
98+
99+
// `trait_variant` will generate a conceptual subtrait:
100+
101+
trait IntFactory: Send {
102+
fn make(&self) -> impl Future<Output = i32> + Send;
103+
fn stream(&self) -> impl Iterator<Item = i32> + Send;
104+
fn call(&self) -> u32;
105+
}
106+
```
107+
108+
In this example, a type that implements `IntFactory` also satisfies the requirements for `LocalIntFactory`, but additionally guarantees that the returned types are `Send`.
109+
110+
### The status quo
111+
112+
Today's solutions include:
113+
114+
### Add a supertrait
115+
116+
#### Pros
117+
* Matches the conceptual pattern between the traits: one clearly implies the other.
118+
* The "standard" Rust way of doing things.
119+
120+
#### Cons
121+
* Breaking change.
122+
* Requires implementors to split out the implementation into separate `impl` blocks for each trait.
123+
124+
### Add a blanket impl
125+
126+
Rather than declaring `trait A: B`, one can create sibling traits with a blanket impl:
127+
128+
```rust
129+
trait Supertrait {
130+
// supertrait items
131+
}
132+
133+
trait Subtrait {
134+
// supertrait items + subtrait items
135+
}
136+
137+
impl<T: Subtrait> Supertrait for T {
138+
// impl supertrait items using subtrait items
139+
}
140+
```
141+
142+
#### Pros
143+
* Backwards compatible to introduce `Supertrait`
144+
145+
#### Cons
146+
147+
* Middleware impls are impossible! We'd like to write:
148+
149+
```rust
150+
struct Middeware<T>(T);
151+
152+
impl<T: Supertrait> Supertrait for Middleware<T> { ... }
153+
154+
impl<T: Subtrait> Subtrait for Middleware<T> { ... }
155+
```
156+
157+
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)).
158+
159+
Other nits:
160+
* The directionality of the impl is less clear.
161+
* 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.
162+
163+
### Provide no bridging
164+
165+
Another alternative is to provide two totally separate traits with no bridging, requiring users to manually implement both versions of the trait:
166+
167+
```rust
168+
trait Supertrait { ... }
169+
trait Subtrait { ... }
170+
171+
struct Impl { ... }
172+
173+
impl Supertrait for T { ... }
174+
impl Subtrait for T { ... }
175+
```
176+
177+
#### Pros
178+
179+
* Backwards compatible
180+
* Middleware can be written to bridge either impl
181+
182+
#### Cons
183+
184+
* Requires duplication
185+
* Requires users to restate bounds
186+
* Existing code which implements `Subtrait` cannot be used as `Supertrait`, so APIs which require `Subtrait` cannot be relaxed to `Supertrait`
187+
188+
### The next 6 months
189+
190+
In the next six months, we aim to ship a solution which addresses the `Service` and `Receiver` use-cases
191+
by allowing trait impls to implement supertraits *if* the impl itself contains a definition of an item
192+
from the supertrait.
193+
194+
Traits will have to opt their impls into this behavior, possibly through the use of a keyword.
195+
`auto` is used below as an example keyword:
196+
197+
```rust
198+
// Library code:
199+
trait Subtrait {
200+
fn supertrait_item();
201+
fn subtrait_item();
202+
}
203+
// User code:
204+
impl Subtrait for MyType {
205+
fn supertrait_item() { ... }
206+
fn subtrait_item() { ... }
207+
}
208+
209+
// -- can become --
210+
211+
// Library code:
212+
trait Supertrait {
213+
fn supertrait_item();
214+
}
215+
trait Subtrait: Supertrait {
216+
fn subtrait_item();
217+
}
218+
// User code is unchanged from above, no separate `Supertrait`
219+
// impl required
220+
impl Subtrait for MyType {
221+
fn supertrait_item() { ... }
222+
fn subtrait_item() { ... }
223+
}
224+
```
225+
226+
### The "shiny future" we are working towards
227+
228+
In the future, we'd like it to be backwards-compatible for traits to split out arbitrary, possibly
229+
defaulted items into supertraits.
230+
231+
This will be challenging, as it won't be obvious syntactically whether an impl intends to provide a
232+
supertrait impl-- some degree of coherence / overlap resolution will be required. However, this feature
233+
will provide library authors a great deal of flexibility while allowing for more ergonomic end-user
234+
implementations.
235+
236+
## Ownership and team asks
237+
238+
| Task | Owner(s) or team(s) | Notes |
239+
|------------------------------------------|----------------------|----------------------------------|
240+
| Discussion and moral support | ![Team][] [lang] | |
241+
| Lang-team champion | @cramertj | |
242+
| Discussion and moral support | ![Team][] [compiler] | |
243+
| Discussion and moral support | ![Team][] [types] | |
244+
| Author RFC | @cramertj | |
245+
| Implementation | @cramertj & others | |
246+
| Stabilization decision | ![Team][] [libs-api] | Stabilizing `Receiver`. Unblocked by implementation. |
247+
| Stabilization decision | ![Team][] [lang] | Stabilizing `arbitrary_self_types`. Unblocked by new `Receiver` API. |
248+
249+
### Definitions
250+
251+
For definitions for terms used above, see the [About > Team Asks](https://rust-lang.github.io/rust-project-goals/about/team_asks.html) page.
252+
253+
* *Discussion and moral support* is the lowest level offering, basically committing the team to nothing but good vibes and general support for this endeavor.
254+
* *Author RFC* and *Implementation* means actually writing the code, document, whatever.
255+
* *Design meeting* means holding a synchronous meeting to review a proposal and provide feedback (no decision expected).
256+
* *RFC decisions* means reviewing an RFC and deciding whether to accept.
257+
* *Org decisions* means reaching a decision on an organizational or policy matter.
258+
* *Secondary review* of an RFC means that the team is "tangentially" involved in the RFC and should be expected to briefly review.
259+
* *Stabilizations* means reviewing a stabilization and report and deciding whether to stabilize.
260+
* *Standard reviews* refers to reviews for PRs against the repository; these PRs are not expected to be unduly large or complicated.
261+
* *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.
262+
* *Dedicated review* means identifying an individual (or group of individuals) who will review the changes, as they're expected to require significant context.
263+
* Other kinds of decisions:
264+
* [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.
265+
* 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.
266+
* Library [API Change Proposal (ACP)](https://std-dev-guide.rust-lang.org/development/feature-lifecycle.html) describes a change to the standard library.

0 commit comments

Comments
 (0)