Home > database >  Two level inheritance in Rust
Two level inheritance in Rust

Time:08-13

I have a double level of (say inheritance, I am not sure about the exact name in Rust).

           Question
     _________|_________
     |                 |
 Operation         Equation
              _________|_________
              |                 |
          OneVariable     TwoVariables

Well, my understanding is that a branch should be a trait and a leaf should be a struct. Neither a struct can be inherited nor a trait can have values. I reduced the code to the following:

pub trait Question {
    fn new(whatever) -> Self;
}

pub struct Operation{--snip--}

impl Question for Operation {
    fn new(whatever) -> Operation {--snip--}
}

pub trait Equation: Question,
{
    fn new(whatever) -> Self; //Bookmark1
}


pub struct OneVariable {--snip--}

impl Question for OneVariable {--snip--} //Bookmark2
impl Equation for OneVariable {--snip--} //Bookmark3

(And the same for TwoVariables).

Question: Where shall I implement new() for the OneVariable?

CodePudding user response:

I'm unsure what exactly you are trying to achieve, but I think you will become very frustrated if you keep holding on to OOP patterns in Rust. Rust is not meant to be used as an OOP language, and most patterns are only possible to be implemented with a lot of pain or not at all.

The words "parent", "grandparent", "inherit" simply don't apply to Rust and traits. The words are rather "interface definition", "implementation", "abstraction". For example, traits cannot carry data. Every pattern in OOP that assumes that parents are autonomous beings or have any functionality, for that matter, simply don't work in Rust. Yes, traits can have default implementations, but those default implementations can only call other trait functions. They cannot modify any internal data, because, as said, traits do not carry any data.

I've seen that you use new() a lot in your traits. That's something typical in OOP, where to construct a class, you have to call the parent constructor. You can not construct a trait, and you can not call a 'parent' constructor in Rust, because those things simply do not exist. There is no point in having a new() function in a trait.

You simply create an object of a struct, and then the traits it implements specify how that object can be used.

You can implement traits based on other traits, of course, but that requires a rethinking of how your functionality is layed out.

That said, there are great alternative patterns that are exist. But that depends heavily on your usecase, of course.

From what you showed so far, my first instinct is to lay out the trait/struct pattern like this (although this is of course just a feeling, as I don't know what exact usecase you have):

pub trait Question {
    fn do_question_thing(&self);
}

pub trait Equation {
    fn do_equation_thing(&self);
}

pub struct OneVariable {
    data: String,
}

impl OneVariable {
    pub fn new(s: &str) -> Self {
        Self {
            data: s.to_string(),
        }
    }
}

// Make `OneVariable` an `Equation`
impl Equation for OneVariable {
    fn do_equation_thing(&self) {
        println!("Doing equation thing: {:?}", self.data);
    }
}

// Make all `Equation`s be `Question`s
impl<T> Question for T
where
    T: Equation,
{
    fn do_question_thing(&self) {
        println!("Doing a question thing, which triggers:");
        self.do_equation_thing();
    }
}

fn main() {
    let one_variable = OneVariable::new("Hello world!");
    one_variable.do_question_thing();
}
Doing a question thing, which triggers:
Doing equation thing: "Hello world!"

As you can see, it's somewhat defined in reverse compared to OOP. You say "OneThing is an Equation", and then define all the functionality that an Equation has to fullfill. Then, you say "All Equations are Questions" and implement the functionality that Questions need to provide based on the functionality of Equations. OOP would be kind of the other way round.

Then, if you instantiate a OneVariable, it automatically has all the functionality of a Question implemented, if it implements Equation.

This isn't better or worse than OOP, it's just different. You can't do a couple of things with this that you could do with OOP, like abstract and overwrite, but it has big advantages in other places, like pure trait crates for clean abstraction layers between libraries (for example the log crate; if you use it for logging, you can simply swap the log backends as they all implement the same logging traits).

CodePudding user response:

As @Jmb points out, if OneVariable is supposed to be both a Question and an Equation, it'll have to implement both of those traits' methods:

pub trait Question {
    fn new() -> Self;
}

pub trait Equation: Question {
    fn new() -> Self;
}

pub struct Operation {}

impl Question for Operation {
    fn new() -> Self {
        Self {}
    }
}

pub struct OneVariable {}

impl Question for OneVariable {
    fn new() -> Self {
        Self {}
    }
}

impl Equation for OneVariable {
    fn new() -> Self {
        Self {}
    }
}

One trick you can do is to provide a default implementation of new in the supertrait based on the base trait:

pub trait Equation: Question {
    fn new() -> Self
    where
        Self: Sized,
    {
        <Self as Question>::new()
    //  ^^^^^^^^^^^^^^^^^^^^^^^^^
    // use the `new` method we already have by default
    }
}

This does however require the bound Self: Sized on Equation's new method and, of course, only makes sense if the signatures of both new methods are compatible.

  • Related