-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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: Implement parent items with child traits #2303
Conversation
This RFC allows to implement functions, associated types and associated constants to be defined while implementing a child trait. Therefore you have to define them in a special way, using the parent traits.
``` | ||
But in order to allow this, trait `A` needs to be defined in a different way, like this: | ||
``` | ||
trait A: use B+use C { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be cool if there was a way to specify that you want this behavior at the trait implementation, rather than the trait definition. This would allow you write these sorts of impls with existing traits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think, it'd be useful at definition time.
What you think, would be something like that, I assume:
struct Number;
impl Add+Sub for Number {
fn add(…) {…}
fn sub(…) {…}
}
This may simplify implementation, but doesn't help with compatibility.
Maybe in combination with trait aliasses this would be useful.
When you want to split your trait A
into B
and C
, like in the example from the RFC, you could write this:
trait B{…}
trait C{…}
trait A_old{…}
trait A = B+C+A_old;
This would be useful for backward compatibility too, but you would have to split it just into new traits without the ability to leave something inside the original trait. Also you probably couldn't create dynamic versions of this type then, anymore, which then would break it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My immediate reaction was that this seemed like a natural and neat extension which would save me a lot of boilerplate. However, I also think that boilerplate would be significantly reduced by existing (unimplemented) features such as implied bounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think implied bounds was the feature, that you don't have to write where clauses in every implementation, if you write it in the type definition itself, wasn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@porky11 Yes.
[reference-level-explanation]: #reference-level-explanation | ||
|
||
This will be implemented by recursively searching for all names of traits, which are used by the current trait. | ||
If one of the names exists at least twice, this will be reported as an error when the trait is defined. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, it's a backwards compatible change to add default methods to a trait. This would cause that to break, since trait Foo: use Bar { fn foo() { ... } }
would break if Bar
added a default method called foo
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, then this may be a bit more complicated, than I thought.
Then my other approach for using something as defined in Alternatives
seems more useful:
trait Bar {
fn bar();
fn foo() {…}
}
trait Foo: Bar {
use Bar::bar; //now bar is implementable when implementing `Foo`
fn foo();
}
Then another problem occurs: It's not possible to use traits, when the user doesn't have to implement one of it's functions, but this was not possible anyway, according to my descriptions.
The child trait is required to use everything of the parent trait, that has no default value. This way, it will be ensured, that adding something with default impl won't be a breaking change now.
C++ allows a very similar thing and it's something that I personally don't like compared to Rust. class Interface1 {
virtual void interface1() = 0;
};
class Interface2 {
virtual void interface2() = 0;
};
class Interface3: Interface1, Interface2 {
virtual void interface3() = 0;
};
class Derived: Interface3 {
// Implement interface 1
void interface1() override {}
// Implement interface 2
void interface2() override {}
// Implement interface 3
void interface3() override {}
// Inherent methods
void derived() {}
}; I think this is okay if |
It seems like the strongest argument for this feature is that it (arguably, maybe) makes it backwards-compatible to "split" a trait into several smaller traits. So my main question is: How often does this use case really come up in public APIs? I don't use Rust very often, but every time I've looked at a Rust crate's docs all the traits seemed to have only one or two or occasionally three required methods. Some of them do have a ton of defaulted methods, but I have a hard time imagining a scenario where you'd want to move defaulted methods to a separate smaller trait, and it wouldn't be obvious prior to a 1.0 release. |
I dislike this right now because it complicates finding the I do favor ensuring that child traits can provide specializations for parent traits under more scenarios. Right now, there are nasty cases where a child cannot provide a specializations for parent with an associated type or else doing it cause recursion overflows, but those are specialization bugs not new features. |
@Ixrec |
@Ixrec Actually just the other day I hit a case where I wanted to split a trait into two, though I don't think this RFC would solve the problem (it's sort of tangentially related, and not tied to backcompat). The situation was something like this: trait Foo {
type Arg;
type Ret;
fn bar(&self, x: Self::Arg) -> Self::Ret;
} I wanted to change the trait so that trait FooBase {
type Ret;
}
trait Foo<Arg>: FooBase {
fn bar(&self, x: Self::Arg) -> Self::Ret;
} Now, however, types which implement |
@cramertj |
The motivation section is rather brief and I'm not sure I understand it. I think I get the ergonomics point - if your type has a lot of parameters with bounds and such it can be quite annoying to reiterate this. However, "implied bounds" alleviates a lot of this pain. This second motivation is intriguing, but I don't understand what you mean:
Can you elaborate on this, maybe show some examples? |
@withoutboats Assuming, you define a trait for objects moving in space. You can access the position, the velocity and displace the object by some delta and accelerated by some force:
Now you use this trait in many places, maybe even other people use this crate.
But now you would also like to be able to use the functions, you define for your
This also works for most cases. But it's not, what you would like to have. If it wouldn't break anything, you would have used different design, but when you don't want to break, you cannot change it anymore.
This would cause an error like that: Another example is, you have multiple traits, that you want to implement a new parent trait for, when you found, that multiple traits share some functions, here a Drawable, which also has a position and can be displaced. Probably a stupid example, but there may be other cases, where this could really happen.
This also is not possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've definitely encountered the problem of wanting to split traits up. I've also, as @cramertj noted, encountered the problem on the other side: wanting to define traits that I can define "all at once" without distinct impls. I suppose though that often this could be defined at the trait level.
Regarding the semver questions, it seems like we would only allow one trait to "use" another if they are in the same crate.
All that said, I'm not sure about the keyword choice -- something is tickling me about this proposal, but I'm not sure what it is. Basically, we want to define a kind of "hierarchy of related traits" -- this feels like something we've wanted in other contexts too.
@nikomatsakis If restricted to a single crate, maybe it's worth thinking about implicitely allowing this, but this would probably require a new RFC. |
I could imagine being explicit |
@burdges Why |
Just my mistake |
@burdges |
While I understand the rationale behind the RFC, I don't think this helps understanding the code in any way, because now one must remember the super traits of all implemented traits at all time. I feel like this optimises for things that are just a bit painful to write when they are implemented the first time, but will hinder understanding those very same impls a few weeks later. My experience implementing a massive amount of traits for a massive amount of data types in Servo and in particular Stylo makes me conclude that this is unneeded and potentially harmful. |
I was thinking about this the other day. I would like to propose an alternative. I think we should generalize the syntax of impl<..> Trait1<P1..Pn> + Trait2<P1..Pn> for P0 This would be exactly equivalent to independent impls: impl<..> Trait1<P1..Pn> for P0 { .. }
impl<..> Trait2<P1..Pn> for P0 { .. } where the generic parameters and items are divided accordingly. There are some things to work out:
If we did this, and we combine with trait aliases, then I think we get a lot of the goals of this RFC. For example, we could (I think) backwards compatibly change trait Foo {
fn a();
fn b();
} into trait Foo = Foo1 + Foo2;
trait Foo1 { fn a(); }
trait Foo2 { fn b(); } |
Not having something in this space is why .Net has For example, I wish we could generalize |
@rfcbot fcp postpone My feeling on this RFC is that it is tackling the right problem but the design is not quite there yet. Therefore, I'd prefer to postpone in favor of more discussion on internals or elsewhere. I do want to thank @porky11 for the interesting idea, though. Some concerns about the specific form of this RFC:
Personally, I'm enthusiastic about this direction of being able to leverage trait aliases, but I'm open to other ideas. It certainly has its complications too: for example, I just realized that the trait alias approach runs into the semver concerns that @cramertj mentioned, which seem more problematic in that context. |
Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged teams: No concerns currently listed. Once a majority of reviewers approve (and none object), 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! See this document for info about what commands tagged team members can give me. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period is now complete. |
Closing per completed FCP with a motion to postpone. Thanks to @porky11 for the RFC =) |
This RFC allows to implement functions, associated types and associated constants to be defined while implementing a child trait.
Therefore you have to define them in a special way, using the parent traits.
Rendered