How to implement inheritance-like feature for Rust?

I read through all "the book" and it writes that no inheritance in Rust.
However, can we implement a feature similar to inheritance for Rust?
And why wouldn't Rust allow inheritance even just single inheritance like that in Java?
The reason I ask these two question is that I feel traits seems as powerful as inheritance because public attributes and methods can be inherited in Java by child classes and therefore inherited methods can use inherited attributes with no doubt, but traits only can ensure that methods of it and its super traits are implemented while attributes are not guaranteed, so the default method implementations of a trait seem to be weak.

Like we can do this without extra implementation in Java

public class People
{ int eatenApple =0;
   public void eatApple(){ eatenApple++; System.out.printf("%d apple eaten",eatenApple); }
}

public class Student extends People
{ //other attributes and methods specificly related to Student
}

// in main
Student s = new Student();
s.eatApple();

However we seem not to be able to do this without extra implementation for type Student in Rust.
Say we define a trait EatApple like below, we still need to set an attribute for type Student and implement the trait with exactly the same code.

pub trait EatApple{
    fn eat_apple(){ println!("by default I don't know how many apples were eaten"); }
}

If we can define attributes(not only const and static like in Java) in traits, than we can do the similar thing as the inheritance dose in this example in Java. Like

pub trait EatApplePlus{
     eaten_apple : i32;
     fn eat_apple(){ println!("{} apple eaten", eaten_apple); }
}
6 Likes

I think it's better to think about traits as java interfaces rather than a base class. The same constraints apply to both:

  • you can have associated constants
  • and methods the type has to implement (or roll with the default)
pub trait EatApple {
  fn apples_eaten(&self) -> i32;
  fn eat_apple(&self) {
    println!("I've eaten {} apple(s).", self.apples_eaten())
  }
}
1 Like

Rust tends to model things with 'has-a' relationships (composition) instead of 'is-a' relationships (inheritance). Your original example would likely look something like this:

struct People {
    eaten_apples: i32,
}

impl People {
    fn eat_apple(&mut self) {
        self.eaten_apples += 1;
        println!("{} apples eaten", self.eaten_apples);
    }
}

struct Student {
    people: People,
}

impl Student {
    fn eat_apple(&mut self) {
        people.eat_apple();
    }
}

It's a little more verbose, admittedly, but I generally find this has a lot fewer pitfalls than inheritance in practice.

11 Likes

Indeed, traits are more like interfaces than classes. You don't store "fields" or "data" in a trait, you describe functionality in it. You would put your data in another type declaration, like a struct or an enum. Then you would externally implement your trait for that struct or enum, etc. In order to achieve a true object-oriented style, you'll need to use traits and you want to compose them in a way that allows them to be used as "trait objects". This means that each fn in the trait needs to have &self of &mut self, basically "getters" and "setters" for your "object".

https://doc.rust-lang.org/book/ch17-02-trait-objects.html
https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

5 Likes

You could compose this with trait objects like so:

trait People {
  fn get_eaten_apples(&self) -> usize;
  fn set_eaten_apples(&mut self, value: usize);
  fn eat_apples(&mut self) {
    let eaten_apples = self.get_eaten_apples();
    self.set_eaten_apples(eaten_apples + 1);
  }
}

trait Student: People {
  // override People's default eat_apples
  fn eat_apples(&mut self) {
    let eaten_apples = self.get_eaten_apples();
    self.set_eaten_apples(eaten_apples + 2); // student is hungry
  }
}
2 Likes

Inheritance constrains what you can do a lot more than traits.
Especially single inheritance becomes complicated when you start getting multiple "logical" baseclasses.
E.g. if you want to store all your stuff in a database, everything could `extends DatabaseObject". But then you later add a base class for formatting, and another base class for logging..

You eventually end up with one "god class" as base, that does way too many things (violates single responsibility principle), or your class hierarchy becomes incredibly complex, rigid and hard to refactor.

If you have shared logic in Rust, you can always put it in helper functions, because unlike Java, functions are a first class citizen (you can have "free" functions, without a class)

The "Behavioural Modelling" koan says it better than I ever could:

8 Likes
11 Likes

I think Catherine West does a very good job of explaining the problems with OOP and especially inheritance, with practical code examples, here: RustConf 2018 - Closing Keynote - Using Rust For Game Development by Catherine West - YouTube

The overall message is that OOP is a bad idea and Rust does it's best to discourage one from doing it.

8 Likes

Here are two of my favorite posts on the more-encompassing subject of coming to Rust with an OOP background:
Is Rust OOP? If not, what is it oriented to?
How to Become a Rust Super-developer

4 Likes

Yeah, I understand that traits are more like interfaces, and that's indeed an alternative. However, I think it will be clearer that I just sepcify attributes a trait needs to implement bacause, I think a get_val() is not as clear as val : type when users try to understand what this trait is doing

Indeed. But I think traits that allow to specify attributes needed to implement can be more powerful than the current ones and be able to mimic inheritance and avoid some drawbacks of inheritance.

Yeah, that's an alternative. But with the logic of "has a" instead of "it's a", this example will be a bit weird to understand-- what dose that mean "Student has a People"??

2 Likes

Yeah, the one to one conversion isn't ideal here - you'd be better off naming stuff to reflect it's function. For example, given that the function of People is to manage the state of the apples, I'd rename it to something like Apples. Then the relationship is 'a Student has Apples', which makes a lot more sense.

Just like interfaces, you implement behavior, but with traits you can also extend types which is not defined by you, this also means that you can't enforce what attributes/members a type has or how it's stored. This way traits allow to extend what you can do with a type without changing the content of it, unlike inheritance, and more powerful than interfaces, since you don't have to wrap an existing type just to add additional interface constraints like in Java (except in some cases, but see orphan rules).

In case of inheritance, you'd have an extra member in your class eaten_apples, while you might keep track of different "food types" in a general fashion, like what a DietingPerson has eaten using e.g.: a HashMap.

Inheritance will forces how the data is stored in your class, while an interface/trait defines how you access or act on the data regardless of implementation. It also means that if you have a 0 size type, you can still implement a behavior and say "I ain't eaten a thing today!" without requiring that type to have a field that is constant 0.

struct Person {
    pub eaten_apples: i32,
    pub other_food: i32,
}

struct DietingPerson {
    pub foods: std::collections::HashMap<String, i32>,
}

pub trait ApplesEaten {
    fn get_apples(&self) -> i32;
    fn eat_apples(&self) {
        println!("apples eaten: {}", self.get_apples())
    }
}

impl ApplesEaten for std::vec::Vec<Person> {
    fn get_apples(&self) -> i32 {
        let mut apples = 0;
        for person in self.iter() {
            apples += person.eaten_apples;
        }
        apples
    }
}

impl ApplesEaten for DietingPerson {
    fn get_apples(&self) -> i32 {
        *self.foods.get("apple".into()).unwrap_or(&0)
    }
}
4 Likes

Make sense. In fact, I think, if we just want default fields and methods to lazily avoid writing the same code by deriving a child class via inheritance, we can actually define a macro to do the copy and paste.
We can make it as a syntax sugar, like

struct People {
    eaten_apples: i32,
}
impl People {
    fn eat_apple(&mut self) {
        self.eaten_apples += 1;
        println!("{} apples eaten", self.eaten_apples);
    }
}

struct Student extends People{ 
// any other fields specificly related to Student
}
impl Student{
// any other methods specificly related to Student
}
// above is actually copy and paste
struct Student {
    eaten_apples: i32,
    // and any other fields specificly related to Student
}
impl Student {
    fn eat_apple(&mut self) {
        self.eaten_apples += 1;
        println!("{} apples eaten", self.eaten_apples);
    }
// and any other methods specificly related to Student
}

And of course, what you said is more generally applicable.

Another problem(which is unrelated to inheritance) is that we cannot hide some implementation details, say, we don't want the user to even know the get_apples() and only eat_apples() is accessible, but we seem not to be able to such an accessibility restriction now.

Oh, but we can!
By default, things are only visible inside the module where they're defined. (Like protected in Java)
You can use pub to make things visible beyond the current module.

So if you define your Student in a mod people { .... }, access is impossible for the rest of the program. You would then do "pub eat_apples()"
Since traits specify a (public) interface, you'd leave "get_apples" out of there (implementation detail! Maybe another implementor wants get_eaten_foods("apple"))

Most toy programs have only one default implicit module, so maybe you haven't stumbled on this yet. Hopefully The Book chapter on modules will help you understand them!

p.s. short note on discourse forum etiquette: you can reply to multiple users in a single post! Select the text you want to reply to, hit the "quote" button that appears, and it will be added to your currently-in-progress. This keeps the topic noise-freez and makes it even clearer what you are replying to :slight_smile:

Edit to add:

Even better: derive/implement the Default trait, and cascade your initialisation using the .. "struct literal update syntax" like so:

let options = SomeOptions { foo: 42, ..Default::default() };

This syntax was made to support the entire composition workflow.
Then you don't need complicated macros, and can even selectively override some of the fields you are compositing :smile:

5 Likes

What I meant in "we seem not to be able to do such an accessibility restriction" is like below

mod eat // created by A
{
    pub trait EatApple
    {
        fn get_apple(&self) -> usize;
        fn set_apple(&mut self, num:usize);
        fn eat_apple(&mut self)
        {
            self.set_apple(self.get_apple()+1);
            println!("{} apple eaten",self.get_apple());
        }
    }
}
mod student //created by B
{
    pub struct Student
    {
        apple : usize
    }
    impl crate::eat::EatApple for Student
    {
        fn get_apple(&self)->usize
        {
            self.apple
        }
        fn set_apple(&mut self, num:usize)
        {
            self.apple = num;
        }
    }
}

mod test // created by C
{
    use crate::eat::EatApple;
    fn main()
    {
        let mut student = crate::student::Student{apple:0};
        student.eat_apple();
        // methods defined in the trait but we don't want to expose
        student.set_apple(1);
        student.get_apple();
    }
}

In the trait EatApple, maybe the method eat_apple() is the only one we want to expose, but since eat_apple() is dependent on set_apple() and get_apple(), they will also be exposed, which is not so good. If we can make set_apple() and get_apple() private to the implemented type, users will still need to implement all these three methods for a customed type but won't risk exposing data to others using this customed type.

It seems that implementing Default trait can only give default values for a type, and struct literal update syntax will assign default values from other variables, but what I meant in ''want default fields and methods to lazily avoid writing the same code by deriving a child class via inheritance" is like the code on the top

public class People
{ int eatenApple =0;
   public void eatApple(){ eatenApple++; System.out.printf("%d apple eaten",eatenApple); }
}

public class Student extends People
{ //other attributes and methods specificly related to Student
}

In Java, via inheritance, we can lazily implement the class Student with attribute int eatenApple =0; and method eatApple() from super class People, not having to repeat writing the same code.
So in Rust, I also want to achieve such a feature so that I can define a type People with eaten_apple : u8 and eat_apple(), and then define a type Student and "inherit" from People to avoid I repeating specifying the same field and method. Therefore, we could write a macro, say derive, then we can use like below and let the macro do the copy and paste.

struct People{......}
#[Student derive People]
struct Student{}

It looks like you are contradicting yourself here: if you don't want to expose get_apple, then don't require it as part of your public interface.
A trait is a public interface. By definition it should only list things that must be exposed.

If you want to have get_apple/set_apple to be implementation details, leave them out of the interface, like so:

trait AppleEater {
  fn eat_apple(&mut self)
}

struct Student {
  eaten_apples: usize
}

impl Student { # student-specific functions
  fn get_apples(...) { ...}
  fn set_apples(...) {...}
}

impl AppleEater for Student {
  fn eat_apple(&mut self) {
     self.set_apples(self.get_apples()+1) # probably a reborrowing problem
     # better way: self.apples += 1
  }
}

Composition would be that we make a reusable struct AppleStatus { apples:usize = 0}, that impls AppleEater with all its details.
This struct is then used as a field in both Student, and in Teacher, and in DietingPerson, and in everyone else who needs to eat apples.
Each Person than does a trivial "telescoping" impl AppleEater that passes through the call to the internal self.appleStatus.eat_apple().

With inheritance, you have a special, magic parent. This enforced hierarchy has a LOT of annoying edge cases in bigher, long-lived projects. To make one small change, you must often completely redesign your hierarchy (lots of work!)
With composition you have just another boring field and a pass-through method. This makes refactoring very easy, because there is no huge hierarchy to take into account.

With inheritance, you get matrioshka-dolls: a class extending a baseclass extending a baseclass extending a baseclass extending a ....

With composition, it is like Lego blocks: you build a Student out of an AppleStatus block, next to a TwoLeggedWalker block, next to a UniversityMember block, and a BurgerFlipperEmployee block, and a PoliticalOpinion block and a BeerStatus block and a ....

In code:

struct AppleStatus { apples: usize = 0 }
impl AppleEater for AppleStatus {
  fn eat_apple(&mut self) { self.apples += 1 }
}

struct Student {
  appleStatus: AppleStatus,
  job: BurgerFlipperEmployee,
  home: DingyApartment,
}
impl AppleEater for Student {
  fn eat_apple(&mut self) {
     self.appleStatus.eat_apple()
  }
}

struct Teacher {
  appleStatus: AppleStatus,
  job: UniversityProfessor,
  home: SuburbVilla,
}
impl AppleEater for Teacher {
  fn eat_apple(&mut self) {
     self.appleStatus.eat_apple()
  }
}
3 Likes