How do I mimic/design this specific inheritance behaviour in rust?

First of all the obligatory "I am new to Rust": Which I am :smiley:.

So I have the following problem:

I have two(or more) structs of data, that all implement some common behaviour in addition their own behaviour. I have a list of these structs (or rather: of the 'supertype'), I need to access some of their shared behaviour and some of their individual behaviour.
My Question is: how do I do that in Rust.

To further illustrate my question I have come up with a code comparision between Kotlin and Rust. Kotlin works as I want it to, Rust does not (yet).

In Kotlin the code may look like this(using the plain, old inheritance abstraction):

interface Animal {
    fun eat()
    fun sleep()
}
class Cat(val name: String) : Animal {
    fun backflip()              { println("jump backwards on face") }

    override fun eat()      { println("cat $name is eating fish(or lasagne)") }
    override fun sleep()    { println("cat $name sleeps inside") }
}
class Lion(val tag_id: Int) : Animal {
    fun roar()              { println("roar") }

    override fun eat()      { println("lion(tag=${tag_id} is eating gazelle") }
    override fun sleep()    { println("lion(tag=${tag_id} sleeps outside") }
}

var animals: MutableList<Animal> = ArrayList()
fun main() {
    animals.add(Cat("Garfield"))
    animals.add(Lion(12))
    //later:
    for (animal in animals) {
        animal.sleep()
        if (animal is Lion)
            animal.roar()
    }
}

In Rust I came up with the following code (which does not allow an 'instance_of' type function):

trait Animal {
    fn eat(&self);
    fn sleep(&self);
}

struct Cat {
    name: String
}
impl Cat {
    fn backflip(&self)      { println!("jump backwards on face") }
}
impl Animal for Cat {
    fn eat(&self)       { println!("cat {} is eating fish(or lasagne)", self.name) }
    fn sleep(&self)     { println!("cat {} sleeps inside", self.name) }
}

struct Lion {
    tag_id: usize
}
impl Lion {
    fn roar(&self)      { println!("roar") }
}
impl Animal for Lion {
    fn eat(&self)       { println!("lion(tag={}) is eating a gazelle", self.tag_id) }
    fn sleep(&self)     { println!("lion(tag={}) sleeps outside", self.tag_id) }
}

fn main() {
    let animals:Vec<Box<dyn Animal>> = vec![
                Box::new(Cat {name: "Garfield".to_string()}),
                Box::new(Lion {tag_id: 12})
    ];
    //later:
    for animal in animals {
        animal.sleep()
        //HOW WOULD I ACCESS THE CONCRETE STRUCT HERE?
    }
}

Playground

After some help from another forum (shh), I have also come up with the following. I would right away like to point out that I don't like that solution very much. The self match for every method in the Animal trait in the enum impl is excessively verbose. Additionally I cannot easily access newly created cats or lions by their specific type.

I nonetheless wanted to show what I have already tried:

trait AnimalTraits {
    fn eat(&self);
    fn sleep(&self);
}

enum Animal {
    Cat(Cat),
    Lion(Lion)
}

impl AnimalTraits for Animal {
    fn eat(&self) {
        match self {
            Animal::Lion(l) => l.eat(),
            Animal::Cat(c) => c.eat()
        };
    }

    fn sleep(&self) {
        match self {
            Animal::Lion(l) => l.sleep(),
            Animal::Cat(c) => c.sleep()
        };
    }
}

struct Cat {
    name: String
}
impl Cat {
    fn backflip(&self)      { println!("jump backwards on face") }
    fn new(name: String) -> Animal {
        Animal::Cat(Cat { name })
    }
}
impl AnimalTraits for Cat {
    fn eat(&self)       { println!("cat {} is eating a gazelle", self.name) }
    fn sleep(&self)     { println!("cat {} sleeps outside", self.name) }
}

struct Lion {
    tag_id: usize
}
impl Lion {
    fn roar(&self)      { println!("roar") }
    fn new(tag_id: usize) -> Animal {
        Animal::Lion(Lion { tag_id })
    }
}
impl AnimalTraits for Lion {
    fn eat(&self)       { println!("lion(tag={}) is eating fish(or lasagne)", self.tag_id) }
    fn sleep(&self)     { println!("lion(tag={}) sleeps inside", self.tag_id) }
}

fn main() {
    let animals = vec![Cat::new("Garfield".to_string()), Lion::new(12)];
    //later:
    for animal in animals {
        animal.sleep();
        match animal {
            Animal::Lion(lion) => {
                lion.roar()
            }
            _ => {}
        };
    }
}

Playground (2)

First of all, checking the concrete type behind an interface or a base class is an anti-pattern even in classic OO languages that do allow for inheritance. (Which might be old, but I certainly wouldn't call it "plain" either – however, that's a whole different discussion.)

The proper solution instead is to implement a method on each type that behaves according to the concrete type, and call it through the abstract interface. This might involve not doing anything at all, like Dog::roar(). (The code above is obviously a toy example, so implementing a MaybeRoarIfLion interface for all types including non-lions might sound bad, and would in fact be bad, but in real situations there's usually a lot more coherence between related types.)

You might even want to separate this behavior further from the types themselves, in which case you should probably look into the visitor pattern.


However, there is in fact a way to go from an erased type back to a concrete tpe: Box::<dyn Any>::downcast() and its variants like Any::downcast_ref(), which you can use for dynamic type checking and, well, downcasting dyn Any to a concrete type.

3 Likes

It might be an anti-pattern, but it does beat cluttering the common trait with functions for the individual types, doesn't it? I mean each function that is only used by a subset of the trait implementors has to be in the super trait.. That can get out of hand pretty quickly, even in real situations.

It just seems a little weird to me, is all. In Rust that would also involve wrapping the possible return type into an Optional and default implement with None, right? Unless I want to have a method in the super trait for every implementing individual implementers to check whether the trait object is actually of that type.

The visitor pattern seems a little overkill and it will involve a ton of boilerplate code for such a simple task.

See the following code adapted by your suggestion:

trait Animal {
    fn eat(&self);
    fn sleep(&self);

    fn roar_if_possible(&self) {  }
    fn get_num_hair_balls_if_possible(&self) -> Option<usize> {
        None
    }
}

struct Cat {
    name: String
}
impl Animal for Cat {
    fn eat(&self)       { println!("cat {} is eating fish(or lasagne)", self.name) }
    fn sleep(&self)     { println!("cat {} sleeps inside", self.name) }

    fn get_num_hair_balls_if_possible(&self) -> Option<usize> {
        return Some(100)
    }
}

struct Lion {
    tag_id: usize
}
impl Animal for Lion {
    fn eat(&self)       { println!("lion(tag={}) is gazelles", self.tag_id) }
    fn sleep(&self)     { println!("lion(tag={}) sleeps outside", self.tag_id) }

    fn roar_if_possible(&self) {
        println!("roar")
    }
}

fn main() {
    let animals:Vec<Box<dyn Animal>> = vec![Box::new(Cat{name: "Garfield".to_string()}), Box::new(Lion{tag_id:12})];
    //later:
    for animal in animals {
        animal.sleep();
        animal.roar_if_possible();
    }
}

Playground (3)

Is there not a more Rust way or overall better way? I am not fixated on inheritance or OO here, I just want a list of trait objects, access their common functions, but also be able to go access the individual functions.
If that does not work I'll need to rewrite a considerable portion of my code :sweat_smile:
Tbh, maybe this kind of OO just isn't applicable and I'll have to focus my control flow more on functions - would be an interesting challenge to my OO-damaged way of thinking. :sweat_smile:

Thanks for your help.

Again, because the code is a toy example, I'm not sure what your actual use case is. If you tell us what you are trying to do, rather than the purported "how", we might be able to offer higher-level guidance. I would not like to speculate too much further as to what a more or less elegant solution is for your specific need, because it might be meaningless or downright bad advice.

3 Likes

I think of this claim as dubious because "there's more than one way to bake a cake."

I would strongly argue for clean code architecture that doesn't try to mix irrelevant types under a single umbrella. Going along with the synthetic Animal class for illustration, when does it actually make sense to call animal.roar() on an instance of Dog? Wouldn't the code in fact be more robust if it didn't have to do any sort of runtime reflection to determine whether the type can roar()?

From this point of view, Vec<Box<dyn Animal>> is almost nonsensical. A more appropriate construct might be Vec<Carnivore>. Counterarguments for a Carnivore that doesn't roar() could conceivably exist, so this isn't ideal either. How about Vec<RoaringAnimal> as a compromise?

My point is that type erasure and dynamic dispatch has its place, but polluting various methods with conditional calls is just line noise that you might be better off without.

2 Likes

My use case(which I very much hope is minimal, while illustrating my issue) is the following:

I have a trait 'Item' that has a name (by getter).

trait Item {
    fn get_name() -> String;
}

One implementor of Item is the ItemList, a struct that contains a name and a vector of other Items.

struct ItemList {
    name: String,
    items: Vec<Box<Item>>
}
impl Item for ItemList { ... }

These items may again be ItemLists or a second implementor of Item, the CallableItem.

struct QueryableItem {
    name: String
}
impl Item for CallableItem { ... }
impl QueryableItem {
    fn query() -> Option<i32> {
        None //or some call
    }
}

Now I put ItemLists in ItemLists and QueryableItems in those and so on, until I get a sort of tree structure. Note that QueryableItem and ItemList are not going to necessarily remain the only two types I want to populate this tree structure with. So I cannot reasonably keep for example two vectors in an ItemList, both of sub item lists and all queryableitems - because that would clash with extensibility.

Now from the root ItemList I want to recursively traverse this tree, query all results from queryable items and put them in a vector.

This is what I attempted to minimally illustrate with the trait object list of animals that roar(or don't). I though keeping it general might help more people that find this topic in the future.

On a side note: In Kotlin I would have a super type that has a name and a getter for said name, inherited by all subclasses - is there a convenient+equivalent concept in rust that relieves me of the boilerplate that all implementors have to both keep a name variable and the implementation for that?

The help is appreciated

You can implement a data tree with a common interface trait:

trait Node {
	fn name(&self) -> &str;
	fn children(&self) -> &[&dyn Node];
	fn query(&self) -> Option<i32>;
}

And you can store all the objects in the tree with a single shared interface via dyn Node (or dyn Item if you prefer) but it's not really good practice conflate this 'flat' interface sharing with your hierarchical data traversal, because they serve very different purposes. In Rust it is fairly easy to do hierarchical data traversal, but very hard to make hierarchically extensible interfaces. (And I would argue that the latter is unneeded: if a user can extend your interface in ways you can't anticipate, then there's no way for you to actually use these extensions. So the user may as well declare their own interface and operate on that instead, rather than trying to extend yours.)

Not directly. You can provide a default implementation for a trait, but if you want implementors of that trait to defer to other implementors, you must write that out explicitly.

1 Like

I think the first example here is why a lot of OOP design gets into a mess. Classifying things is hard. In the real world classes can be very nebulous and even change in definition. Things can get reclassified. A famous recent case: Is Pluto a planet?

Once you have created a class hierarchy in your OOP language you are kind of stuck with it. Likely you find out you have not made good choices in your class distinctions and abstractions.

In the example we see an exceptional situation that needs a work around already. A lion is in the class of animal but it's determined that only lions roar so there is an "if" around that. All of a sudden knowledge of what any given animal does is not in the animal but in the owner, as it were.

But what if instead of the specific "roar()" method we had a "makeWarningSound()" method, for example. With that simple change in abstraction we see that all animals could have a "makeWarningSound" method. It would cause a lion to roar, a cat to hiss, a dog to bark and perhaps be an empty function for others. Now the owner of the animal does not need to know what each animal does when you poke it in the ear with a stick. We have reduced the coupling between owner and animal.

Now of course, "makeWarningSound()" sounds like something many things not in the animal class could do. Like a truck reversing or a smoke detector. Oops (pun intended). Maybe all this classification was not such a good idea.

Hmm... sounds like a trait to me...

2 Likes

Perahps its time to trot out the second Rust koan.

6 Likes

Alright, so you have a tree and you want to traverse it.

And that indeed changes the picture a lot.

Usually, since node types in a tree form a closed set of possible nodes, this is a good fit for using an enum. And then, incidentally, the common properties such as the name can be factored out into a wrapper struct:

struct Node {
    name: String,
    item: Item,
}

enum Item {
    ItemList {
        items: Vec<Node>,
    },
    QueryableItem {
    }
}

That is, unless you want to allow users and external crates to extend the set of possible types in nodes, in which case you do indeed need a trait, and you should indeed implement it for every node type, because if the tree is fully dynamic, then there has to be some dynamic type checking happening at some point. However, you can still cut down on boilerplate quite a bit by providing a default implementation for query():

trait Item {
    fn name(&self) -> &str; // this is more idiomatic

    /// Returns `None` if node is not queryable.
    fn query(&self) -> Option<i32> {
        None
    }
}

struct ItemList {
    name: String,
    items: Vec<Box<dyn Item>>,
}

impl Item for ItemList {
    fn name(&self) -> &str {
        &self.name
    }

    // No need to explicitly impl `<ItemList as Item>::query()` here
}

struct QueryableItem {
    name: String,
    value: i32,
}

impl Item for QueryableItem {
    fn name(&self) -> &str {
        &self.name
    }

    fn query(&self) -> Option<i32> {
        Some(self.value)
    }
}

In this manner, you'll be able to just ask any node if it's queryable, and if so, it will return the result of the query. You won't need to perform checks against concrete types, and you won't get into the ugly "check if thing is possible and if so, do the thing" pattern; instead you will use the "try the thing unconditionally and see if it succeeded" pattern.

This is what the Python world calls "Easier to Ask for Forgiveness than Permission", and in general it's a great way of avoiding various kinds of bugs, e.g. TOCTOU in the case of file operations.

You can even combine this latter approach with the former of bundling the common properties in a Node that wraps the rest, but in this case the Node would of course contain a Box<dyn Item> instead.

If you find that your code is still repetitive because you keep implementing name() and query() in similar ways over and over again, then you might want to write a #[derive(Item)] procedural macro for automatically generating non-trivial impls.

2 Likes

Thanks a lot! I guess I'll do it this (the first, enum) way.

struct Node {
    name: String,
    item: Item,
}

enum Item {
    ItemList {
        items: Vec<Node>,
    },
    QueryableItem {
        value: i32
    },
    ExecutableItem {}
}

impl Node {
    fn new_root(name: &str, items: Vec<Node>) -> Node {
        Node::new_list(name, items)
    }
    fn new_list(name: &str, items: Vec<Node>) -> Node {
        Node {
            name: name.to_string(),
            item: Item::ItemList {
                items
            }
        }
    }

    fn new_queryable_item(name: &str, value: i32) -> Node {
        Node {
            name: name.to_string(),
            item: Item::QueryableItem {
                value
            }
        }
    }

    fn sum_all_queryable_in_tree(&self) -> Option<i32> {
        return match &self.item {
            Item::ItemList {items } => {
                let mut counter = 0;
                for item in items {
                    if let Some(result) = item.sum_all_queryable_in_tree() {
                        counter += result;
                    }
                }
                Some(counter)
            }
            Item::QueryableItem {value} => {
                Some(*value)
            }
            _ => None
        }
    }
}

fn main() {
    let root = Node::new_root("Root",
                              vec![
            Node::new_list("Sub 1",
                           vec![
                               Node::new_queryable_item("Query 1.1", 1),
                               Node::new_queryable_item("Query 1.2", 2)
                          ]
            ),
            Node::new_list("Sub 2",
                           vec![
                               Node::new_queryable_item("Query 2.1", 3),
                               Node::new_queryable_item("Query 2.2", 4)
                ]
            )
        ]
    );

    println!("Sum of Queryable: {:?}", root.sum_all_queryable_in_tree())
}

Playground (4)

My only remaining objection is the fact that my 'sum_all_queryable_in_tree' function is available at compile time to all different variants of Item. I think it would be desirable to have the compiler check that it is only accessed if the Item is a ItemList or QueryableItem.. Is that at all possible?

Additionally/on the same note: I cannot implement specific behaviour for an enum variant, right?
So if I had a trait ItemBehaviour, that has a function 'doSomething', I would have to impl that for node and do a match on the specific Item?

I mean yes OOP-inheritance-design is bad if a hierarchy is not actually a good way to separate your data/behaviour and naturally things can change...

But in the case that a hierarchy does make sense, which you have to admit it sometimes does, OOP-inheritance-design can be useful.

I'll do concede that it is an over used feature, though. And that it is currently occupying a way too large part of my brain particularly.

We should perhaps not get into the pros and cons of OOP here. That is a topic that has become a huge debate all over the internet in the last few years. Typing "OOP Considered Harmful" into Google search is quite eye opening.

Personally I have never understood the obsession with OOP since I first discovered it with C++ and then Java. And certainly did not understand the compulsory "everything must be in a class" of Java. So I feel it is a debate that is long overdue. Why didn't everyone see things my way? Was I just too dumb to get the everything is OOP idea? People just did not want to talk about it and seemed to look at you like your an idiot for even questioning the idea. Now they do. So I don't feel so daft.

No doubt OOP has it's place. Rust has chosen a different path that is all. All is well.

1 Like

Possible, maybe, desirable, certainly not. The very purpose of an enum is to allow dynamic choice between types. Why would you want to create an enum just to then restrict it to only one of its variants if your tree is dynamic? That seems to be the exact opposite of the requirement you previously stated. I'm confused, and it seems to me that the language is not at fault here, but there's some other latent design choice and/or expectation that you are not telling us.

1 Like

Yes, I additionally need some abstract behaviour. I.e. Every node should implement it, but depending on the item the actual implmentation might differ.

That seems to be a very natural use of a trait.
NodeBehaviour, for example containing the method 'expand' or something similar.

All Nodes need this behaviour, but what that means differs by their Item variant.

This can be solved using your second solution which already has something similar to 'expand': 'get_name'. Though then I'd run back into the 'issue' of cluttering my NodeBehaviour or in this example Item trait with the individual or partially implemented methods (that just returns None for most implementations).

However I guess that might just not be as big an issue as I make it out to be.

Then why do you want to "restrict" the use of this method to a single variant?

I thought we were talking about this:

On the other note: I essentially want my cake and eat it too... I want to access to the specific method in ItemList (sum_all_queryable_in_tree), but also be able to decide whether something even is an ItemList from a list of more generic 'super' objects (the nodes or items).
I'm sorry I think I am still too much in the hierarchy thinking.
Rust is different and that is fine. I just don't think like that yet in terms of design.

If this is a hard requirement, you can break up the enum variant values into individual structs and implement a trait on the structs that need it. The not-so-great thing about this strategy is that you end up requiring extra pattern matching at the call sites, and it may not be worth the effort. playground

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.