Provides a mechanism to declare "inherent traits" for a type defined in the same crate. Methods on these traits are callable on instances of the specified type without needing to import the trait.
There are two similar cases where this is valuable:
Frequently used traits.
Sometimes getting the right abstractions require breaking up a type's implementation into many traits, with only a few methods per trait. Every use of such a type results in a large number of imports to ensure the correct traits are in scope. If such a type is used frequently, then this burden quickly becomes a pain point for users of the API, especially if users do not care about writing generic code over traits.
Mapping object-oriented APIs.
When mapping these APIs to rust, base classes are usually mapped to traits: methods on those base classes will need to be callable on any derived type. This is sub-optimal because to use a class method a user must now know which class in the hierachy defined that method, so that they can import and use the corresponding trait. This knowledge is not required when using the same API from an object-oriented language.
The feature is implemented using a new attribute which can be applied to trait impl
blocks:
pub trait Bar {
fn bar(&self);
}
pub trait ExtBar: Bar {
fn ext_bar(&self);
}
impl<T: Bar> ExtBar for T {
fn ext_bar(&self) { }
}
pub struct Foo;
#[inherent]
impl Bar for Foo {
fn bar(&self) { println!("foo::bar"); }
}
// works thanks to specialization
#[inherent]
impl ExtBar for Foo { }
impl Foo {
fn foo(&self) { println!("foo::foo"); }
}
The methods bar
and ext_bar
are now callable on any instance of Foo
, regardless of whether the Bar
and ExtBar
traits are currently in scope, or even whether the Bar
trait is publically visible. In other words if Bar
and ExtBar
defined in one crate and Foo
and another, user of Foo
will be able to explicitly depend only on the crate which defines Foo
and still use the inherent traits methods.
The inherent
attribute in the above example makes the impl
block equivalent to:
impl Foo {
#[inline]
pub fn bar(&self) { <Self as Bar>::bar(self); }
fn foo(&self) { println!("foo::foo"); }
}
Any questions regarding coherence, visibility or syntax can be resolved by comparing against this expansion, although the feature need not be implemented as an expansion within the compiler.
T
or on an impl T { .. }
block.The most viable alternative is delegation proposal, although arguably inherent traits and delegation solve different problems with the similar end result. The former allows to use trait methods without importing traits and the latter to delegate methods to the selected field. Nethertheless delegation RFC and this RFC can be composable with each other:
struct Foo1;
struct Foo2;
#[inherent]
impl T1 for Foo1 {
fn a() {}
}
#[inherent]
impl T2 for Foo2 {
fn b() {}
}
struct Bar {
f1: Foo1,
f2: Foo2,
}
impl Bar {
// all methods from `T1` will be delegated as well
// though `T1` will not be implemented for `Bar`
delegate * to f1;
}
// method `b` will be accessable on `Bar` without importing `T2`
#[inherent]
impl T2 for Bar {
delegate * to f2;
}
#[inherent]
? In other words do we want to write inherent impl A for B { .. }
?Attendance: nikomatsakis, Josh, TC
nikomatsakis: I think this is a good idea. What were the concerns that blocked us from going forward in the past?
TC: Some of the answer is in the rationale and alternatives, e.g., overlap with delegation.N Do we think there is any overlap?
Josh: I'm not sure I see the overlap with delegation for the things we want inherent traits for.
nikomatsakis: I think that delegation covers a lot more – I always thought of it more like "delegating to a field", rather than "delegating to a trait". I guess at the end if #[inherent]
becomes syntactic sugar for some kind of delegations, I can live with that.
nikomatsakis: We currently can't introduce traits without boilerplate (duplicating methods). This seems to solve the problem. Does it? Am I missing something?
joshtriplett: Breaking change here being requiring to import the trait?
nikomatsakis: Yes, and ambiguity.
joshtriplett: Are we confident it's not a breaking change to extract methods into a trait and make that trait impl inherent?
nikomatsakis / TC: Seems like yes given the desugaring in the RFC.
JT:
Potential ambiguity, how does it resolve?
struct Type { ... }
trait T1 {
fn method();
}
trait T2 {
fn method();
}
impl T1 for Type { ... }
impl T2 for Type { ... }
// In another crate:
use firstcrate::{Type, T2};
let x: firstcrate::Type = get_a_type();
x.method();
TC: It's equivalent to adding the method directly, so it would fall under what we call "trivial breakage".
Josh: We could use a different desugaring that gives the inherent trait methods the priority of traits rater than inherent methods.
Advantage: can't silently change behavior of existing code.
Advantage: same resolution order as a trait method.
Disadvantage: can't compatibly migrate from inherent method to inherent trait.
TC: Keep in mind that the person writing the trait/impl may be different than the person using the trait and type and bringing them into scope. When moving an inherent method to a trait and using the #[inherent]
attribute, the first person can know this will be compatible under the proposed desugaring. But otherwise that person cannot know this.
NM: That's convincing.
Josh: New warnings will show up for people who have imported the trait and now no longer need to. Are we OK with that?
NM: Yeah, we want the warning.
Josh: Agreed.
NM: Let's analyze some cases that come up in the standard library.
Case 1a:
struct Foo { }
impl Foo {
fn method(&self);
}
trait Bar {
fn method(&self);
}
impl Bar for Foo {
fn method(&self);
}
// want to add
// trait Baz { }
// #[inherent]
// impl Baz for Foo { }
Here: Foo::method
wins, we could move this to #[inherent] impl Baz for Foo { .. }
, would retain semantics (and would still win).
Case 2:
struct Foo { }
impl Foo {
fn method(&self) {
Bar::method(self)
}
}
trait Bar {
fn method(&self);
}
// add `#[inherent]` here
impl Bar for Foo {
fn method(&self);
}
This is OK iff Foo::method
already calls Bar::method
Case 3:
struct Foo { }
trait Bar {
fn method(&self);
}
// want to add `#[inherent]` to this
impl Bar for Foo {
fn method(&self);
}
NM: I agree this is not a breaking change. If there was another trait with method
, it'd already be ambiguous, so not a breaking change.
JT: but they might not have imported Bar
, so their use of the other trait is not ambiguous.
NM: ah, ok. yes, you're right.
Josh: Do we care about removing extraneous items from the RFC (e.g. reference to specialization)?
Josh: Because this is an old RFC, let's just propose and commit those changes directly.
Consensus: We'll propose and commit directly any editorial changes we have to the RFC.
TC: We seem to have consensus this is a good idea. I propose that we propose FCP merge. Even if we want some editorial changes to the RFC, we could do that during that process.
Josh: I'll do that.
Consensus: We like this RFC with the current proposed desugaring and will propose FCP merge.
(The meeting ended here.)
TC: The draft RFC doesn't mention associated constants at all. If we did not make these inherent when stabilizing inherent trait impls, it would not be backward compatible to do it later.
We probably do want to make associated constants from the trait inherent on the type.
Here's the proposal. We want to update the RFC to say that this:
#[inherent]
impl Trait for Foo {
const CONST: u8 = 1;
}
…desugars to this:
impl Foo {
const CONST: u8 = <Self as Trait>::CONST;
}
TC: The draft RFC doesn't mention associated types at all. Since inherent associated types are not yet stable, we don't necessarily need to make a decision here, but we should think about it. When we do stabilize inherent associated types, we would need to commit to the behavior.
Here's the proposal. Let's update the RFC to say that when the inherent associated types behavior in RFC 195 becomes stable, that this:
#[inherent]
impl Trait for Foo {
type Assoc = Self;
}
…will desugar to this:
impl Foo {
type Assoc = <Self as Trait>::Assoc;
}
TC: In the discussion above, we had left two major items unresolved.
(Part of the motivation for wanting to allow only some items to be made inherent is to prevent or to fix breakage caused when a trait later adds a new method with a default implementation whose name conflicts with the name of an existing inherent method.)
Coming up with a syntax for these that combines well with the #[inherent]
attribute could be challenging.
One alternative that would make solving these problems straightforward is to add some syntax to the inherent impl
block for the type. Given the desugaring in the RFC, there is some conceptual appeal here. (quaternic proposed this arrangement; TC is proposing the concrete syntax.)
We can use use
syntax to make this concise and intuitive.
Here's an example:
trait Trait1<Tag, T> {
fn method0(&self) -> u8 { 0 }
fn method1(&self) -> u8 { 1 }
}
trait Trait2<Tag, T> {
fn method2(&self) -> u8 { 2 }
fn method3(&self) -> u8 { 3 }
fn method4(&self) -> u8 { 4 }
}
struct Tag;
struct Foo<T>(T);
impl<T> Foo<T> {
// All methods and associated items of Trait1 become inherent,
// except for `method0`. The inherent items are only visible
// within this crate.
pub(crate) use Trait1<Tag, T>::*;
// Only `method2` and `method3` on Trait2 become inherent.
pub use Trait2<Tag, T>::{method2, method3};
fn method0(&self) -> u64 { u64::MAX }
}
impl<T> Trait1<Tag, T> for Foo<T> {}
impl<U: Trait1<Tag, T>, T> Trait2<Tag, T> for U {}
This solves another problem that we discussed above. How do we prevent breakage in downstream crates when a trait later adds a new method with a default implementation? Since a downstream crate might have made an impl of this trait for some local type inherent and might have an inherent method with a conflicting name, this could be breaking.
We already handle this correctly for use
declarations with wildcards. Any locally-defined items override an item that would otherwise be brought into scope with a wildcard import. We can reuse that same behavior and intuition here. When a wildcard is used to make all items in the trait inherent, any locally-defined inherent items in the impl
prevent those items from the trait with the same name from being made inherent.
Advantages:
use Trait::*
) makes it very intuitive what exactly is happening and what exactly your API is promising.use
syntax makes it natural for a locally-defined item to override an item from the wildcard import because that's exactly how other use
declarations work.rust-analyzer
would probably support expanding a wildcard use Trait::*
to an explicit use Trait::{ .. }
just as it does for other use
declarations, which would help people to avoid breakage.use
, pub use
, pub(crate) use
, etc.) for the items made inherent.Disadvantages: