-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Allow a trait to implement its parent trait #1024
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
Comments
Does a blanket-impl suffice? impl<T: Circle> Shape for T {
fn area(&self) -> f64 {
self.radius().powf(2.0) * 3.14
}
} |
No. Only a single Consider the following example with trait Shape { fn area(&self) -> f64;}
trait Circle : Shape {
fn radius(&self) -> f64;
}
struct MyCircle;
impl Circle for MyCircle {
fn radius(&self) -> f64 {
2.0
}
}
impl<T: Circle> Shape for T {
fn area(&self) -> f64 {
self.radius().powf(2.0) * 3.14
}
}
trait Square : Shape {
fn size(&self) -> f64;
}
impl<T: Square> Shape for T {
fn area(&self) -> f64 {
self.size() * self.size()
}
}
fn main() {
use Shape;
let b = MyCircle;
println!("{}",b.area());
} $ rustc test.rs
rustc test.rs
test.rs:16:1: 20:2 error: conflicting implementations for trait `Shape` [E0119]
test.rs:16 impl<T: Circle> Shape for T {
test.rs:17 fn area(&self) -> f64 {
test.rs:18 self.radius().powf(2.0) * 3.14
test.rs:19 }
test.rs:20 }
test.rs:26:1: 30:2 note: note conflicting implementation here
test.rs:26 impl<T: Square> Shape for T {
test.rs:27 fn area(&self) -> f64 {
test.rs:28 self.size() * self.size()
test.rs:29 }
test.rs:30 } This fails because the compiler is not sure some struct is not both a Meanwhile under the present proposal, the compiler can walk from |
@drewcrawford But the same problem occurs here: what if you have a third trait, "Square", which also implements "Shape", and then have a type for which both "Square" and "Circle" are implemented - you have two conflicting implementations of "area". In this simple case the compiler could simply error, but what if there are generic impls for "Square" and "Circle", and a downstream create happens to declare a type which matches both generic impls? It introduces a whole new set of ways in which traits could be invalid. |
No, a different problem occurs here.
Now you might say "Well, we should change the language to do deferred type checking and allow multiple Minimally, the compiler needs to see if two traits both implement trait Circle : Shape {
fn radius(&self) -> f64;
fn area(&self) -> f64 { //here we supply an implementation for Shape
self.radius().powf(2.0) * 3.14
}
} Then whether or not the trait implements Meanwhile if we try to do |
The syntax you're using Either you try to prevent the conflict early, by disallowing both a Square and a Circle generic trait implementation in the same program, or you wait until you find a concrete, monomorphised type which implements both before erroring. In the first case, it means you now have crates which can no longer be used together in the same application (exactly what the coherence rules are there to prevent). In the second case, you break forward compatibility, because even just adding additional trait implementations to upstream crates can break code downstream (again, what the coherence rules are there to prevent). For this to work, you'd have to come up with a set of sane coherence rules, and I don't think that's possible in this case. A better explanation might be this: currently the two traits, "Square" and "Circle" can effectively share the "Shape" implementation they both depend on. Under your scheme they can no longer share the same "Shape" implementation because they place additional requirements on the trait beyond it just being implemented (it specifically has to be implemented according to their implementation). These additional requirements don't exist anywhere else in the language. |
The following code fails to compile on Rust nightly: trait Shape { fn area(&self) -> f64;}
trait Circle : Shape {
fn radius(&self) -> f64;
}
impl<T> Shape for T where T : Circle {
fn area(&self) -> f64 {
self.radius().powf(2.0) * 3.14
}
}
impl<T> Shape for T where T : Shape {
fn area(&self) -> f64 {
self.size() * self.size()
}
}
fn main() {
} Perhaps whatever you mean by "This is currently perfectly valid today" would be clearer if you modified this program so that it compiles.
Sure, but changing almost anything upstream can break code downstream. I don't see how this feature especially contributes to that problem in any meaningful way. |
Well it doesn't compile because you changed the code I wrote? trait Shape { fn area(&self) -> f64;}
trait SomeOtherTrait { fn do_something(&self); }
trait Circle : Shape {
fn radius(&self) -> f64;
}
trait Square : Shape {
fn edge_length(&self) -> f64;
}
impl<T> Circle for T where T : Shape + SomeOtherTrait {
fn radius(&self) -> f64 {
42.0
}
}
impl<T> Square for T where T : Shape + SomeOtherTrait {
fn edge_length(&self) -> f64 {
1.0
}
}
fn main() {
} That compiles fine, and I can make a type "Foo" which implements "Shape + SomeOtherTrait", and it's still fine, because Square and Circle can share the same implementation of Shape, ie. they don't add additional constraints on their parent trait.
Well it's supposed to be the case that adding new trait implementations can be done in a backwards compatible manner. Look at why the negative trait bound RFC has not been accepted. |
Well sure, that compiles, but it doesn't have anything to do with the motivating problem:
In your approach, I can't write struct MyCircle;
impl Circle for MyCircle { fn radius(&self) -> f64 { 2.0 } } because error: the trait `Shape` is not implemented for the type `MyCircle` [E0277]
test.rs:26 impl Circle for MyCircle { } Instead I would have to write out the π*r^2 formula for every struct that implements
And that's the case under this proposal as well. More formally, if we have a ShapeLibrary that declares pub trait Circle : Shape {
fn radius(&self) -> f64;
fn area(&self) -> f64 { //here we supply an implementation for Shape
self.radius().powf(2.0) * 3.14
}
} then our ClientProgram can declare a struct ProgramCircle;
impl Circle for ProgramCircle { fn radius(&self) -> f64 { 2.0 } } When our ShapeLibrary later adds some NonEuclidianCircle: pub trait NonEuclidianCircle : Shape {
fn radius(&self) -> f64;
fn area(&self) -> f64 { //here we supply an implementation for Shape
8 / 3.14 * self.radius().powf(2.0) //area of a circle under the negation of Euclid's 5th postulate
}
} Since our ClientProgram does not use |
I have the same problem in my application. I cannot express my logic in a clear way and I have to copy paste methods like the I COULD hack around it, but then I'd need negative trait bounds which Rust doesn't have either |
For the time being, I'm using the preprocessor to work around. This has some unhappy side effects, namely
Via SO somebody proposed a workaround involving associated items, https://stackoverflow.com/a/29292642 That workaround is novel but it increases the difficulty of manually implementing the trait (as opposed to implementing it from another trait). My problem is such that I have a mix of both cases, so the drawbacks of the preprocessor are preferable to me. But if you don't mix "trait-implemented" and "directly-implemented" traits it might be an approach to study. |
For what it's worth, I think this is not something the trait system is well-suited to support, and may want to wait for an OOP facility to be included in the language. |
@aidancully: it has nothing to do with that, we already have trait inheritance. The proposals there are for struct/enum inheritance. The proposal here is to satisfy the It seems to be a default method oversight that this already doesn't work. |
@iopq, I see where you're coming from, but as @Diggsey says, allowing a default implementation for a base trait function allows either coherence or forward-compat issues when you have diamond inheritance of traits. I mentioned the OOP proposals because OOP can more naturally provide a mechanism for dealing with diamond inheritance (either by explicitly disallowing it, or by forcing users to specifically choose how to resolve an inheritance conflict) than traits can. I also think that this use case ( |
Is there any update on this issue? I have the same problem when implementing a transparent proxy layer for user trait objects. I have to force developers to add a macro for convenience which is very annoying. |
Appears impossible as stated. See comments by @aidancully and @Diggsey above I'd think specialization resolves some issues around this. Say via
It requires that overlapping Also, there are cases where trait parameters or associated types can give you the needed disjointness properties, like in https://stackoverflow.com/a/29292642 There are still efforts towards an acceptably limited form of negative reasoning too. See Niko's comment #1672 (comment) |
Is it a goal of this proposal to allow new parent trait implementations that don't require any changes to structs implementing the child trait? If not, could something like the following work? pub trait Shape {
fn area(&self) -> f64;
}
pub trait Circle : Shape {
fn radius(&self) -> f64;
}
optin impl<T : Circle> Shape for T {
fn area(&self) -> f64 {
self.radius().powf(2.0) * 3.14
}
}
struct MyCircle;
impl Circle for MyCircle {
fn radius(&self) -> f64 {
1.0
}
}
impl Shape for MyCircle with Circle; There are two parts to the suggestion:
For larger traits, that could save a lot of boilerplate code, but it doesn't seem to alter the semantics of determining which impl applies. It also seems fairly tooling-friendly: The compiler would be able to suggest |
What specifically is the issue you see here? This is squarely the sort of use case specialization is supposed to solve, so the more examples the better of things someone might want to do, but can't given the ordering rules we've considered. |
Afaik there is no "issue" here @withoutboats I just restating in a confusing way. Yes, if you want a examples for an argument for the lattice rule, then I suppose these parent trait cases can easily give you a couple. |
https://github.com/hainish/multi-default-trait-impl may be useful for some use case subset of the requested feature. It's a procedural macro which allows you to define multiple default implementations of a trait. |
What's the current recommended way to do this, given that currently a subtrait cannot provide a default implementation for a supertrait? |
The Rust Reference says:
Traditionally this is accomplished by adding an implementation to the underlying struct, and thus the trait
Circle
can only be applied to structs that supply an implementation forShape
.However, I think it is very often useful (including in the example in the reference) to allow the trait to supply the supertrait implementation all by itself. In the reference example, does it really make sense for every
struct : Circle
to provide its own implementation ofarea
? No. The area of a circle is always π*r^2.Notably, it is still true that "types that implement
Circle
must also have an implementation forShape
." The difference is whether this implementation is supplied by each struct separately, or byCircle
itself. The latter is better DRY.Proposed syntax:
The text was updated successfully, but these errors were encountered: