How to implement inheritance the Rust way?

I don't know how to solve the following problem, which would otherwise be trivial with the use of inheritance and subtyping, and I'm clearly having difficulties to switch to the Rust way (Composition over Inheritance?).
So here is the code where I define two structs Square and Triangle which implement functionality from a Trait Polygon:

trait Polygon {
    fn perimeter(&self) -> f32;
}

struct Square {
    side: f32,
}

struct Triangle {
    side1: f32,
    side2: f32,
    side3: f32,
}

impl Polygon for Square {
    fn perimeter(&self) -> f32 {
        self.side * 4f32
    }
}

impl Polygon for Triangle {
    fn perimeter(&self) -> f32 {
        self.side1 + self.side2 + self.side3
    }
}

Now, for some unfathomable reason, I would like to create a vector-like Struct, let's call it Polygons containing both Squares and Triangles, i.e. Polygons.
So, I've wrote:

struct Polygons<T: Polygon> {
    polygons: Vec<T>,
}

impl<T: Polygon> Polygons<T> {

    fn new() -> Self {
        Self{
            polygons: Vec::new()
        }
    }

    fn add(&mut self, poly: T) {
        self.polygons.push(poly)
    }
}

fn main() {
    let mut polygons: Polygons<Polygon> = Polygons::new();
}

But as you all know, this code doesn't build, since I get the following errors:

error[E0782]: trait objects must include the `dyn` keyword
  --> src/bin/traits.rs:45:32
   |
45 |     let mut polygons: Polygons<Polygon> = Polygons::new();
   |                                ^^^^^^^
   |
help: add `dyn` keyword before this trait
   |
45 |     let mut polygons: Polygons<dyn Polygon> = Polygons::new();
   |                                +++

error[E0277]: the size for values of type `dyn Polygon` cannot be known at compilation time
  --> src/bin/traits.rs:45:23
   |
45 |     let mut polygons: Polygons<Polygon> = Polygons::new();
   |                       ^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn Polygon`
note: required by a bound in `Polygons`
  --> src/bin/traits.rs:27:17
   |
27 | struct Polygons<T: Polygon> {
   |                 ^ required by this bound in `Polygons`
help: consider relaxing the implicit `Sized` restriction
   |
27 | struct Polygons<T: Polygon + ?Sized> {
   |                            ++++++++

error[E0277]: the size for values of type `dyn Polygon` cannot be known at compilation time
  --> src/bin/traits.rs:45:43
   |
45 |     let mut polygons: Polygons<Polygon> = Polygons::new();
   |                                           ^^^^^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn Polygon`
note: required by a bound in `Polygons::<T>::new`
  --> src/bin/traits.rs:31:6
   |
31 | impl<T: Polygon> Polygons<T> {
   |      ^ required by this bound in `Polygons::<T>::new`
32 | 
33 |     fn new() -> Self {
   |        --- required by a bound in this

error[E0277]: the size for values of type `dyn Polygon` cannot be known at compilation time
  --> src/bin/traits.rs:45:43
   |
45 |     let mut polygons: Polygons<Polygon> = Polygons::new();
   |                                           ^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn Polygon`
note: required by a bound in `Polygons`
  --> src/bin/traits.rs:27:17
   |
27 | struct Polygons<T: Polygon> {
   |                 ^ required by this bound in `Polygons`
help: consider relaxing the implicit `Sized` restriction
   |
27 | struct Polygons<T: Polygon + ?Sized> {
   |                            ++++++++

Still, applying the above suggestions doesn't ameliorate the result, but just creates more issues.
Now, what would be the correct rust way of achieving my goal?
Thank you guys for your attention.

1 Like

How about:

enum Polygon {
    Square(Square),
    Triangle(Triangle),
}

impl Polygon {
    fn perimeter(&self) -> f32 {
          match &self {
                Polygon::Square(s) => 4 * s.side,
                Polygon::Triangle(t) => t.side1 + t.side2 + t.side3,
          }
     }
}

// Now just store the Polygon's in a Vec

Nope. Composition and Sum types over Inheritance.

4 Likes

If you want to say: "owned object that implements the trait Polygon but the underlying concrete type might be anything", that's spelled Box<dyn Polygon>. It's called a trait object, and it's not the same as generics. (It is not the same as inheritance, either. It's an existential type: a concrete static type that stands in for another underlying, concrete, but "dynamic" type.)

As currently standing, your Polygons type is generic: it accepts a type parameter T, which, consequently, is a single concrete type. But that's not what you want – you want a collection of heterogeneous (in terms of concrete types) polygons, which however is uniform because all elements are Polygons. Thus, you likely wanted a non-generic collection of trait objects:

#[derive(Default)]
struct Polygons {
    polygons: Vec<Box<dyn Polygon>>,
}

impl Polygons {
    fn add<T: Polygon + 'static>(&mut self, poly: T) {
        self.polygons.push(Box::new(poly));
    }
}

fn main() {
    let mut polygons = Polygons::default();
}

Please read the documentation too.

7 Likes

Thanks for the reply.
Now implementing your solution as follows:

trait Polygon {
    fn perimeter(&self) -> f32;
}

struct Square {
    side: f32,
}

struct Triangle {
    side1: f32,
    side2: f32,
    side3: f32,
}

impl Polygon for Square {
    fn perimeter(&self) -> f32 {
        self.side * 4f32
    }
}

impl Polygon for Triangle {
    fn perimeter(&self) -> f32 {
        self.side1 + self.side2 + self.side3
    }
}

#[derive(Default, Debug)]
struct Polygons {
    polygons: Vec<Box<dyn Polygon>>,
}

impl Polygons {
    fn add<T: Polygon>(&mut self, poly: T) {
        self.polygons.push(Box::new(poly));
    }
}

fn main() {
    let mut polygons = Polygons::default();
    polygons.add(Square{side: 1.0});
}

I get the following error messages:

error[E0310]: the parameter type `T` may not live long enough
  --> src/bin/traits.rs:54:28
   |
53 |     fn add<T: Polygon>(&mut self, poly: T) {
   |            -- help: consider adding an explicit lifetime bound...: `T: 'static +`
54 |         self.polygons.push(Box::new(poly));
   |                            ^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds

error[E0277]: `(dyn Polygon + 'static)` doesn't implement `Debug`
  --> src/bin/traits.rs:49:5
   |
47 | #[derive(Default, Debug)]
   |                   ----- in this derive macro expansion
48 | struct Polygons {
49 |     polygons: Vec<Box<dyn Polygon>>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Polygon + 'static)` cannot be formatted using `{:?}` because it doesn't implement `Debug`
   |
   = help: the trait `Debug` is not implemented for `(dyn Polygon + 'static)`
   = note: this error originates in the derive macro `Debug` (in Nightly builds, run with -Z macro-backtrace for more info)

Correcting; i.e.:

#[derive(Default, Debug)]
struct Polygons {
    polygons: Vec<Box<dyn Polygon>>,
}

impl Polygons {
    fn add<T: 'static + Polygon>(&mut self, poly: T) {
        self.polygons.push(Box::new(poly));
    }
}

I get

error[E0277]: `(dyn Polygon + 'static)` doesn't implement `Debug`
  --> src/bin/traits.rs:49:5
   |
47 | #[derive(Default, Debug)]
   |                   ----- in this derive macro expansion
48 | struct Polygons {
49 |     polygons: Vec<Box<dyn Polygon>>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Polygon + 'static)` cannot be formatted using `{:?}` because it doesn't implement `Debug`
   |
   = help: the trait `Debug` is not implemented for `(dyn Polygon + 'static)`
   = note: this error originates in the derive macro `Debug` (in Nightly builds, run with -Z macro-backtrace for more info)

Oh, and thank you for the reference.
:blush:

I corrected the code in the meantime. The Polygons type can't be Debug as-is.

1 Like

Sorry, for some reason VSCode inserted that Debug in the `#[derive()]. My bad.

Thanks for your reply. Extremely interesting.

To answer the thread's original question (and be a bit of a smart arse), the way you "implement inheritance the Rust way" is by not trying to implement inheritance. Lots of people come to Rust from OO languages and get frustrated by trying to force their OO solutions onto a non-OO language.

Instead, try to change your point of view from an is-a relationship (e.g. a Triangle is a Polygon) to something behaviour-based (e.g. a Triangle has a perimeter that we can calculate). With this formulation, you might create a trait called HasPerimeter with a perimeter() method and implement it for both types.

If you then need to store several different types which all implement this HasPerimeter trait in a Vec, you would let the introduce a layer of indirection with trait objects as mentioned in @H2CO3's response[1].

struct Polygons {
  polygons: Vec<Box<dyn Polygon>>,
}

On the surface, this HasPerimeter might be syntactically identical to your Polygon trait, but the naming changes your thinking away from nested inheritance hierarchies to a more behaviour/composition-based mindset. That means instead of storing a homogeneous objects (Vec<Box<dyn HasPerimeter>>) you might choose to approach it like this:

struct Scene {
  squares: Vec<Square>,
  triangles: Vec<Triangle>,
}

impl Scene {
  fn render(&self, canvas: &mut Canvas) {
    bulk_render_squares(canvas, &self.squares);
    bulk_render_triangles(canvas, &self.triangles);
  }
}

For some applications this approach can be a lot more performant or ergonomic because you are doing bulk operations on homogeneous objects instead of individually asking each object to (in this example) render itself.

Depending on the wider context and whether I need to have access to the original Squares and Triangles I might group them into an enum, which lets me say a Polygon is either a Square or a Triangle. The benefit of this is that you get full access to what a Polygon contains at the cost of requiring a match statement for actually accessing those underlying values.

enum Polygon {
  Square(Square),
  Triangle(Triangle),
}

An OO purist might complain that you are breaking encapsulation here, but I would argue that a) sometimes this is done in contexts where you want full control/visibility and know that adding extra variants is very rare so it's unlikely that you'll have much code churn, and b) Rust isn't an OO language and can be written in the functional style, so SOLID doesn't necessarily apply.

Sorry for the wall of words. Other people have provided several excellent solutions for your problem, so I wanted to explain some of the philosophy behind the various approaches you see in Rust.


  1. This is almost exactly how interfaces are implemented in languages like Go and Java, by the way. You introduce a layer of indirection then alongside that indirection you pass around functions for calling the trait methods that correspond to that object. ↩ī¸Ž

7 Likes

Michael, thank you for your illuminating post. I am fully aware of the fact that I need to change paradigm, and your words are surely helpful to that end.
Have a great one.

1 Like

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.