Inheritance-like with rust trait

Let me just show my simple example

trait Animal {
    fn name(&self) -> &str;
}

trait Dog: Animal {
    fn name(&self) -> &str;
}

trait Cat: Animal {
    fn name(&self) -> &str;
}

struct Husky;
impl Dog for Husky {
    fn name(&self) -> &str {
        "husky"
    }
}
struct Birman;
impl Cat for Birman {
    fn name(&self) -> &str {
        "birman"
    }
}

fn get_name(pet: std::sync::Arc<dyn Animal>) -> String {
    pet.name().to_string()
}

fn main() {
    let husky = std::sync::Arc::new(Husky);
    let birman = std::sync::Arc::new(Birman);
    println!("{}", get_name(husky));
    println!("{}", get_name(birman));
}

I would like to call function like get_name on Parent trait (Animal) to avoid duplicate code for different trait that has the same method. And also, I would like other struct implement child's trait only. In my example, that is we implement name() for Dog and Cat, and Animal's name is automatically implemented.

Is this possible in rust?

There's not really a good way to meet all of your conditions. This is probably what I'd suggest (given the reduction presented).

Some notes:

  • If there's only one subtrait, there's a good way: impl<T: SubTrait> SuperTrait for T. But it doesn't work with multiple subtraits as there would be overlapping implementations in the case of a type that implemented multiple subtraits
  • Regardless of the approach, you don't want to have the same method names for a subtrait and supertrait. Multiple trait methods with the same name and signature will be considered ambiguous regardless of any subtrait/supertrait relationship
3 Likes

You don't need any of this.

You shouldn't try to shove inheritance in Rust, it won't work. Traits should have a 1:1 correspondence to concepts.

If you want to model animals that have a name, make a trait for that, and implement it for entities for which it makes sense:

trait Named {
    fn name(&self) -> &str;
}

impl Named for Husky {
    fn name(&self) -> &str {
        "husky"
    }
}

impl Named for Birman {
    fn name(&self) -> &str {
        "birman"
    }
}

Then, when you need an entity that's nameable, constrain its type with precisely the Named bound and nothing else:

fn print_name<T: Named>(entity: &T) {
    println!("{}", entity.name());
}

There's no need for print_name to know anything about animals, inheritance, or anything complicated like that. State the constraints you need, and program against interfaces.

12 Likes

Since Animal already declares the method name(), the sub-traits don't need to (and shouldn't/can't) override it in their trait declarations. In other words, you can't implement name() for the Dog or Cat traits because they inherit from Animal and name() belongs to Animal.

I updated your code with some notes in this Playground.

And yes.. you can model hierarchical inheritance. Hierarchical behavior inheritance is easy in Rust, but state inheritance is more involved, but still very much possible. If you're curious about modeling both state and behavior inheritance, you can take a look at this post on stackoverflow. However, I wouldn't recommend modelling state inheritance if you can avoid it, and you nearly always can. Only the concrete imlementors (structs that implement the traits) need to have state in the vast majority of situations.

3 Likes

That is ideal solution, but with this I need a breaking change, and it also means we need to implement Named and Dog for one dog struct which is adding complexity for other building their own struct

This is close, the only concern is that we need to implement two concept for one struct.

impl Cat for Birman {}
impl Animal for Birman {

There is nothing wrong with implementing many traits for a given single type. Traits are meant to be used like this.

For internal code usage, I agree with this.
What I'm building is the trait for third party user. We define the trait, and they define their struct.
It is fine if they need to implement multiple trait for their struct, but it is really nice if they can care about the single trait with everything they need.

But, I think how our design also effect simplicity for the user, maybe we need to rethink your trait design.

When you define a trait with trait MyTrait: MySuperTrait { ... } you are declaring that a MyTrait is also a MySuperTrait. Which means that anything that implements MyTrait will also have to declare an impl for MySuperTrait. This doesn't necessarily have to become complex or encumbering. Much of the behaviors could be implemented within the traits themselves, so implementing the supertrait could be as simple as impl MySuperTrait { }, which will satisfy the inheritance constraint MyTrait: MySuperTrait. One real-world example off the top of my head for a trait that has a super-trait would be DoubleEndedIterator: Iterator.

You can't really make both Cat and Dog traits implement (be a sub-trait of) Animal in the OOP sence, because unlike in other languages, there is nothing preventing a type implementing both Cat and Dog. What should then happen when you put that type into a Box<dyn Animal>?

One way to solve this would be to crate a general Dog struct that implements Animal and have every individual dog struct (like Husky for example) include that Dog struct. Then, when implementing Animal for the Husky scruct, just use the Dog's implementation. This is a ton of copy-pasted code, but it can be auto-generated with a macro.

1 Like

I'm not sure what this has to do with boxing, but it's perfectly fine to put a value in a Box even if its type implements many traits, and you can also have multiple supertraits for a dyn Trait.

1 Like

I was sliglty misunderstanding OP's quesrion (I think), I thought what they wanted is to have a trait Dog: Animal with default implementation of get_name returning "dog", such that any struct implementing Dog does not have to re-define get_name and it will return "dog" for that struct, even when it is in a Box<dyn Animal>.

You could do this with one trait with

impl<T: Dog> Animal for T {
    pub fn get_name(&self) -> &str {
        "dog"
    } 
}

but not with two (or more) traits. This also doesn't allow any Dog implemetations to override this default name, but there are ways around that, like adding a get_dog_name method to Dog that returns an option with default inplemetation returing None and then the get_name impl would try to use that.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.