Skip to content
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: inherent trait implementation #2375

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

newpavlov
Copy link
Contributor

@newpavlov newpavlov commented Mar 27, 2018

Continuation of #2309.

Fixes: #1880,#1971

This RFC allows us to write the following code:

#[inherent]
impl Bar for Foo {
    // code
}

Which allows methods from Bar trait to be used on Foo instances without having Bar in the scope.

Rendered

@newpavlov
Copy link
Contributor Author

newpavlov commented Mar 27, 2018

cc @nikomatsakis, @aturon

This RFC assumes that rust-lang/rust#48444 will be treated as a feature and not as a bug.

@aturon
Copy link
Member

aturon commented Mar 27, 2018

To clarify: #2309 was postponed until we figure out the story for delegation. While you talk about it a bit in this new RFC, it's not clear what in this proposal changes the situation compared to when the lang team previous took up this question. Can you expand on your thinking here?

@newpavlov
Copy link
Contributor Author

I was under impression that the previous RFC was closed due to the lack of reaction from @Diggsey to add requested changes, and not postponed. (ctrl+f "postpone" yields zero results)

While I agree that delegation RFC and inherent traits should be discussed together, in my opinion RFCs have orthogonal scopes and similar only in the end effect. As was shown in the text, delegation can be nicely composed with #[inhrent] attribute and there is no need to overload delegation with an additional functionality.

Yes, the new delegation RFC draft mentions inherent trait impls as a possible future extension, but I don't think that using delegate is a correct approach here. Also in my opinion inherent trait implementations can have a much bigger impact on the ecosystem (including stdlib and core) than the delegation RFC, thus it should consider as one of the main topics and not as a vague future extension.

@Diggsey
Copy link
Contributor

Diggsey commented Mar 27, 2018

@newpavlov I don't think that's an accurate assessment. The RFC was closed because:

We discussed this in the lang team meeting today, and everyone agreed it would be nice to provide "inherent trait methods" of some sort. However, we also felt that this feature would probably fall out naturally from a solution to the more general problem of trait and method delegation. We'd like to see another RFC for delegation before we accept a feature like this.

ie. the lang team wanted a more general solution

@scottmcm scottmcm added the T-lang Relevant to the language team, which will review and decide on the RFC. label Mar 28, 2018
@newpavlov
Copy link
Contributor Author

@Diggsey
Yeah, my mistake. Though by reading discussion of the new version of delegation RFC it looks like this use case is not going to "fall out naturally" any time soon.

@aturon
So should I close this RFC until lang-team decides how inherent traits and delegation will interact with each other or should I leave it to be for now?

@aturon
Copy link
Member

aturon commented Apr 19, 2018

@newpavlov Sorry for the delay, was out on vacation.

Yes, I think it should probably be closed for the time being, especially since there's now a delegation RFC. Would be good to revisit after Rust 2018 ships!

@dwijnand
Copy link
Member

Where's the delegation RFC? My searches failed to find anything.

@newpavlov
Copy link
Contributor Author

@dwijnand #2393

@newpavlov
Copy link
Contributor Author

@aturon
How about reopening this PR?

@newpavlov newpavlov reopened this Feb 14, 2019
@Centril Centril added A-traits Trait system related proposals & ideas A-attributes Proposals relating to attributes A-resolve Proposals relating to name resolution. labels Feb 14, 2019
text/0000-inherent-trait-impl.md Show resolved Hide resolved
```

Any questions regarding coherence, visibility or syntax can be resolved by
comparing against this expansion, although the feature need not be implemented
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be explicitly specced out, especially since this expansion actually introduces an ambiguity in method calls.


# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be fleshed out, including:

  • What impl blocks it can be applied to (can the trait be foreign?)
  • Whether it works with #[fundamental]
  • Does it error if you have an inherent method with the same name?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think it should error if you have an inherent method with the same name, like if you had two inherent methods with the same name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What impl blocks it can be applied to (can the trait be foreign?)

Yes, it can. One of the main use-cases for inherent trait implementations is implementation of crates defined in external crates.

Whether it works with #[fundamental]

I am not sure why it should not, though I can't say I fully understand #[fundamental] semantics.

Does it error if you have an inherent method with the same name?

Initially I though it should error, but as I wrote below probably it may be better to issue a warning and shadow inherent trait methods by true inherent methods.

I will try to update the text based on your review! Thanks!

[drawbacks]: #drawbacks

- Increased complexity of the language.
- Hides use of traits from users.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this works the way I think it does, this actually changes our trait stability story, and that's a major drawback.

Currently, adding a defaulted method to a trait is not a major breaking change, at worst it will cause ambiguity errors, so it's considered minor.

Now, a dependency can add a method to a trait you #[inherent] impl, which can clash with a method of your own, causing your build to fail in a way that requires you to change your API to fix. We're largely okay with builds failing due to new ambiguities (clashing method names across traits, adding something to a module that's glob imported, etc) and such things are categorized as minor, which basically means it's fine to do as long as the fallout isn't too much. With this RFC, adding a defaulted method has the potential to break a library user in a way that requires them to rename a method in their public API, causing a breaking change for them.

This should be explored and addressed in this RFC, and as a bare minimum should be called out in this section.

(One "fix" is to only allow local traits)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another fix is to list out the method names that are inherent, although that becomes quite a burden for e.g. iterator methods.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if the existing situation should be classified as so minor because you interact with many types almost exclusively through their traits. As an example, there are definitely traits with a new() method that defies the convention of being inherent new(), so those traits adding new() created exactly the same breakage you describe here.

As a fix, I'd suggest #[inherent] being a method attribute for items inside an impl, so

impl TraitWIthBadMethodNames for MyType {
    fn new() -> Self { .. }  // Not inherent

    #[inherent]
    fn new_plus() -> Self;  // Inherent but default body used

    #[inherent]
    fn foo() { .. }  // Inherent with body supplied here
}

And #[inherent] applied to the impl is equivalent to it being applied to all methods and associated types and constants.

Copy link
Member

@Manishearth Manishearth Mar 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an example, there are definitely traits with a new() method that defies the convention of being inherent new(), so those traits adding new() created exactly the same breakage you describe here.

Yes, but this breakage is easily fixed by using UFCS. This is not true for breakage caused in the world of #[inherent].

The "minor" terminology comes from the API evolution RFC, such changes may cause crates to stop compiling, however:

  • This can be fixed with a trivial change local to the crate
  • The fix does not break upstream crates

Without this notion of "minor" being allowed, crates wouldn't be able to add anything new (types, traits, functions, or methods) without it being considered a breaking change.

Furthermore, the API evolution RFC mentions in the trait method case that you should check to ensure the fallout isn't too great; which is where your new() example falls short: a trait adding a new() method would probably have lots of fallout.

In other words, when I use the term "minor" here, I'm using a precisely defined term from another RFC. Whether or not it is actually "minor" is irrelevant, I'm talking about what we do and don't consider breaking, which we have specced in terms of this major/minor categorization.

Copy link
Contributor Author

@newpavlov newpavlov Apr 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be possible to implement shadowing of inherent trait methods by true inherent methods? Compiler will warn on clashing inherent names while building crate which uses #[inherent], but will use true inherent method by default and for trait method you will have to use explicit Trait::foo(value). This way we will avoid code breakage on "minor" upstream changes.

Though I think in practice such collisions should be extremely rare.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be possible to implement shadowing of inherent trait methods by true inherent methods?

I think that would be the ideal fix here: an inherent trait's methods get shadowed by the type's own methods. They could produce a warning lint when compiling the crate itself, so that people notice, and then cap-lints will suppress that when compiling a dependency.

@burdges
Copy link

burdges commented Mar 28, 2019

How are polymorphic traits handled? I'd presume

#[inherent]
impl<'de> Deserialize<'de> for MyType { .. }

works roughly like

impl MyType {
    fn deserialize<'de,D>(deserializer: D) -> Result<Self, D::Error>
    where D: Deserializer<'de> { ... }
}

Yet, how should separate impl blocks be handled?

#[inherent]
impl Borrow<str> for MyType { }

#[inherent]
impl Borrow<[u8]> for MyType { }

We'd likely want roughly

impl MyType {
    fn borrow<R>(&self) -> R
    where Self: Borrow<R>
    { self.borrow() }
}

@newpavlov
Copy link
Contributor Author

@burdges
I think that initial implementation should simply forbid inherent implementations for polymorphic traits. Later we could do an extension, though I don't see how it can be done nicely right now.

@fbstj
Copy link
Contributor

fbstj commented Jun 7, 2019

is there a reason (that I'm not seeing) that this cannot be done with a macro that just implements the wrapping functions in a generated impl block?

@newpavlov
Copy link
Contributor Author

newpavlov commented Jun 7, 2019

It can be done, but will result in code and documentation bloat. Also use of procedural macros will increase compilation times.

@burdges
Copy link

burdges commented Jun 7, 2019

fn aliases would provide a nice solution here too. I suppose fn aliases should wait until delegation gets sorted out, although argument for waiting is less compelling than for this RFC's proposal.

@Michael-F-Bryan
Copy link

Does this need to be an RFC? As far as I'm aware, it could be implemented using a normal procedural macro and uploaded to crates.io. That also gives us a place to iterate on the implementation before anything is merged.

Kinda like how we used the work from failure to improve std::error::Error's API.

Specialization isn't in stable Rust, and referencing specialization makes the explanation more complex.
@joshtriplett
Copy link
Member

We reviewed this in a @rust-lang/lang meeting today, and would like to move ahead with it.

Regarding the concern of #[inherent] impl LocalTrait for RemoteType, the proposed desugaring in the RFC would make that an error, and we do think that should be an error.

Regarding the concern of #[inherent] impl RemoteTrait for LocalType causing addition of defaulted methods to RemoteTrait being a breaking change, there is a way (albeit annoying) that the crate with that inherent impl can fix such breakage if it arises without changing their API: they could do the desugar manually and only include the methods they want. We could also (as suggested in this thread) add syntax to mark individual methods in a trait impl as #[inherent] rather than the whole impl, which would make that workaround easier. Proposal: we should add that to future work and make it an unresolved question whether we need that syntax before stabilizing.

Another option would be to augment the desugaring to say that "#[inherent] does not add an inherent forwarding method if the type has explicitly defined inherent method of the same name.". You could still get breakage that way if you had multiple #[inherent] blocks and one added a defaulted method conflicting with the other, but that's more far-fetched.

@programmerjake also made the observation that we might want the ability to have a blanket impl for a variety of types and then have a way to make that impl inherent for a specific type. Proposal: let's similarly add that as potential future work.

@rfcbot merge

@rfcbot concern decide-on-handling-for-breakage-with-new-trait-methods

@rfcbot
Copy link
Collaborator

rfcbot commented Sep 13, 2023

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Sep 13, 2023
@OpatrilPeter
Copy link

Apologies if I've missed something obvious -- I can see here a lot of discussion about the mechanical aspects of this feature, but, curiously, I haven't seen much about the plans for usage recommendations, despite the far reaching possibilities.

I feel that, once this feature becomes widely available, "Should I implement this trait for my type inherently or not?" will be a question for every library author, so there should be at least a broad guideline planned before then.

For example, should we say that types existing primarily for satisfying specific traits, such as std::io::BufReader for std::io::Read or std::iter::Iterator for std::iter::Map, should be the ones using it? If so, what about less obvious cases, such as, say, std::iter::IntoIterator for Vec? Where do we want to draw the line?
At extreme end, I can imagine a world where writing inherent impls becomes a recommended style where possible, making Rust feel closer to traditional OOP languages, where classes inherit from interfaces/abstract base classes.

A more concrete point on how being opinionated about usage of this feature could impact things:
If there'd be a convention that inherent impls are expected to "form a fundamental functionality" for given type, then I suspect there's a potential to effectively make the (unstable) documentation feature of "notable traits"[1] obsolete, as we could instead list inherent impls for given type.

[1] As seen, for example, in infobox for return type of Iterator::map.

@nikomatsakis
Copy link
Contributor

Per the discussion on zulip, I have become convinced that it would be better to make this feature use the syntax use, like:

impl SomeType {
    pub use SomeTrait::*;  // re-export the methods for the trait implementation
}

This syntax has a few advantages:

  • We can give preference to explicit method declared in the impl blocks over glob re-exports, eliminating one source of breakage (i.e., trait adds a method with a name that overlaps one of the inherent methods defined on SomeType)
  • Can make just specific methods (not all of them) inherent.
  • Easier to see the inherent method when scanning source.
  • You can re-export with different visibility levels (e.g., pub(crate))
  • It would work best if we planned to permit use SomeTrait::some_method; as a way to import methods as standalone fns, but I wish we did that.

However, in writing this, I realize an obvious disadvantage -- if the trait has more generics and things, it's not obvious how those should map. i.e., consider

struct MyType<T> { 
}

impl<T> MyType<T> {
    pub use MyTrait::foo;
}

impl<T: Debug> MyTrait for MyType<T> {
    fn foo(&self) { }
}

This would be weird -- is this an error, because the impl block says it's for all T? And what if it were trait MyTRait<X>?

@ids1024
Copy link

ids1024 commented Sep 18, 2023

use syntax could also allow using things that aren't part of a trait as associated functions, associated types (rust-lang/rust#8995), etc. Which could occasionally be useful, at least.

If use syntax is extended, but doesn't support some of those things that logically seem like they should be supported, it has the downside of adding more functionality to the language with "orthogonality" issues. Similarly with the use SomeTrait::some_method example.

I like the idea, but to make it feel like a natural part of the language and not an overloading of the use keyword for something else, there are a lot of things to consider.

@RalfJung
Copy link
Member

It would work best if we planned to permit use SomeTrait::some_method; as a way to import methods as standalone fns, but I wish we did that.

I don't entirely understand this sentence, why is there a "but" there? Or is there a negation missing somewhere?

@burdges
Copy link

burdges commented Sep 18, 2023

I'd say drop the type parameters entirely like

impl SomeType pub(crate) use SomeTrait::{some_method};

or even

pub(crate) impl SomeType use SomeTrait::{some_method};

or

pub(crate) use SomeTrait::{some_method} impl SomeType;

@dhardy
Copy link
Contributor

dhardy commented Feb 6, 2024

However, in writing this, I realize an obvious disadvantage -- if the trait has more generics and things, it's not obvious how those should map. i.e., consider

struct MyType<T> { 
}

impl<T> MyType<T> {
    pub use MyTrait::foo;
}

impl<T: Debug> MyTrait for MyType<T> {
    fn foo(&self) { }
}

Lets break this down:

  • If a trait has no extra generics or bounds, no problem.
  • If, as in the example above, the trait has no extra generics but its impl has extra bounds, that should be an error. The fix is easy — replicate those bounds on the "using" impl block — impl<T: Debug> MyType<T> { pub use MyTrait::foo; }.
  • If the trait has extra generics in its name (e.g. From<T>), then different instantiations are different types, so for example From::<u32>::from and From::<i32>::from are not the same method. Thus, one cannot simply pub use From::from; to make it inherent. (Optionally, one could pub use From::<i32>::from; — weird, but it's a fully-qualified path to a method.)
  • If a trait has an associated type, this type is known — so, provided the trait is implemented, there should be no issue re-exporting its methods and associated types: pub use Iterator::*;

That said, the syntax is still weird. I think I'd prefer this:

impl<T: Debug> MyType<T> {
    pub use <Self as MyTrait>::foo;
    // or the fully-qualified variant:
    pub use <MyType<T> as MyTrait>::foo;
}

@petrochenkov
Copy link
Contributor

petrochenkov commented Feb 6, 2024

@dhardy
This almost words on nightly rustc with delegation feature enabled:

#![feature(fn_delegation)]
#![allow(incomplete_features)]

struct MyType<T> { field: T }

trait MyTrait {
    fn foo(&self) {}
    fn bar(&self) {}
    fn baz(&self) {}
}

impl<T: std::fmt::Debug> MyTrait for MyType<T> {}

impl<T: std::fmt::Debug> MyType<T> {
    pub reuse MyTrait::foo;
    pub reuse <Self as MyTrait>::bar;
    pub reuse <MyType<T> as MyTrait>::baz;
}

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=35cde317e177ea36698eb3901988a482

, except generics are not fully supported yet.

UPD: Works without generics - https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=d7d92cc51a2cd01b325f97eaaa65a01e

@CAD97
Copy link

CAD97 commented Apr 17, 2024

re: concern decide-on-handling-for-breakage-with-new-trait-methods:

An alternative formulation without the breakage could be to, instead of desugaring to an additional "real" inherent implementation, have #[inherent] change name resolution for SelfTy to always act as-if the implemented trait were in scope. That is, the implementation can coexist with a "real" inherent implementation of that name, with the "real" inherent implementation shadowing the trait implementation, and causing a name resolution conflict if another trait is in scope also has items with the same name.

#[inherent] impl would still be restricted to only be allowed when a "real" inherent impl could be written, and could still warn if any of the trait associated items are shadowed by "real" inherent impls.

@joshtriplett joshtriplett added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Jul 16, 2024
@joshtriplett
Copy link
Member

Nominating so we can address the possible syntaxes (#[inherent] vs @nikomatsakis' use proposal) and the handling of conflicts, and so that we can see if there are any other blockers to moving forwards with this.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jul 16, 2024

@RalfJung

It would work best if we planned to permit use SomeTrait::some_method; as a way to import methods as standalone fns, but I wish we did that.

I don't entirely understand this sentence, why is there a "but" there? Or is there a negation missing somewhere?

It was phrased poorly. What I meant is: it's inconsistent to permit use in an impl given that we don't support use Trait::method; at module-level (or other kinds of use in impls), but I think I would prefer to fix that by making use of a trait method work in those positions.

@traviscross
Copy link
Contributor

@rfcbot cancel

This FCP has been outstanding so long that let's just cancel it. My sense is that we do want something like this, but that we probably want some revisions to the RFC, perhaps along the lines of the use syntax that I proposed in the design meeting on 2023-09-12 and that Niko seconded (and raised an interesting question regarding) above.

When making these updates and before restarting pFCP, let's be sure that the concern that Josh raised here is addressed (resolving this point was one of the motivations of the use syntax).

@rfcbot
Copy link
Collaborator

rfcbot commented Jul 24, 2024

@traviscross proposal cancelled.

@rfcbot rfcbot removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jul 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-attributes Proposals relating to attributes A-ergonomics A-resolve Proposals relating to name resolution. A-traits Trait system related proposals & ideas I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Inherent trait implementations