• Feature Name: inherent-trait-impl
  • Start Date: 2018-03-27

Summary

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.

Motivation

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.

Guide-level explanation

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.

Reference-level explanation

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.

Drawbacks

  • Increased complexity of the language.
  • Hides use of traits from users.

Rationale and alternatives

  • Define inherent traits either on a type T or on an impl T { .. } block.
  • Implement as part of the delegation RFC.
  • Do nothing: users may choose to workaround the issue by manually performing the expansion if required.

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;
}

Prior art

Unresolved questions

  • Do we want to introduce a new keword instead of using #[inherent]? In other words do we want to write inherent impl A for B { .. }?

Design meeting minutes

Attendance: nikomatsakis, Josh, TC

Why have we not done this already?

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.

Are there gaps in how it solves problems?

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.

Stdlib cases

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.

The RFC has some things that seem like a distraction; should we get them removed before acceptance?

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.

Let's propose FCP merge

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.)


Things that came up after the meeting

Handling of associated constants

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;
}

Handling of associated types

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;
}

Alternate syntax proposal

TC: In the discussion above, we had left two major items unresolved.

  • How do we make blanket trait impls inherent?
  • How can we allow only some items from the trait impl to be made inherent?
    • This is especially tricky for associated functions and methods with a default implementation.

(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:

  • It provides a syntax for adopting as inherent a blanket implementation of a trait for the type.
  • It provides a syntax for specifying which methods should become inherent, including methods with default implementations.
  • The wildcard import (use Trait::*) makes it very intuitive what exactly is happening and what exactly your API is promising.
  • The 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.
  • We can support any visibility (e.g. use, pub use, pub(crate) use, etc.) for the items made inherent.

Disadvantages:

  • There's some redundancy, especially when the items to make inherent are specifically named.
Select a repo