Polymorphism using enums

Hi every one, I'm starting to get into Rust by building a project in which I have to be able to handle multiple types of shapes. Let me explain my solution first and then introduce the problem I'm facing now.

All shapes implement some common properties and functionality. The first thing I did was specifying that common functionality with a Shape trait:

trait Shape {
    fn intersect(&self, material: u32) -> String;
    fn normal_at(&self, material: u32) -> String;
}

(Don't mind that these methods return a String and that material is a number, it's just for demonstration purposes).

I have two concrete types of shapes right now: Plane and Sphere, both of which have some fields in common, and of course, also implement the Shape trait:

struct Plane(Properties);
struct Sphere(Properties);

struct Properties {
    material: u32,
}

impl Shape for Plane {
    fn intersect(&self, material: u32) -> String {
        format!("Plane<{}>.intersect()", material)
    }

    fn normal_at(&self, material: u32) -> String {
        format!("Plane<{}>.normal_at()", material)
    }
}

impl Shape for Sphere {
    fn intersect(&self, material: u32) -> String {
        format!("Sphere<{}>.intersect()", material)
    }

    fn normal_at(&self, material: u32) -> String {
        format!("Sphere<{}>.normal_at()", material)
    }
}

My first problems started to raise when I tried to create a vector of shapes using trait objects: let vector: Vec<Box<dyn Shape>> = Vec::new(). However, I quickly shifted to an enum-based polymorphic solution because using Box I was loosing access to the inner types stored in each element of the vector (idea I found in this blog post). So I ended up with the following:

enum Shapes {
    Sphere(Sphere),
    Plane(Plane),
}

impl Shape for Shapes {
    // not using the material arg right now.
    fn intersect(&self, material: u32) -> String {
        match self {
            Shapes::Plane(p) => p.intersect(p.0.material),
            Shapes::Sphere(s) => s.intersect(s.0.material),
        }
    }

    fn normal_at(&self, material: u32) -> String {
        match self {
            Shapes::Plane(p) => p.normal_at(p.0.material),
            Shapes::Sphere(s) => s.normal_at(s.0.material),
        }
    }
}

With my current implementation I'm able to do something like this:

let shapes = vec![
    Shapes::Plane(Plane(Properties { material: 0 })),
    Shapes::Sphere(Sphere(Properties { material: 1 })),
    Shapes::Sphere(Sphere(Properties { material: 2 })),
    Shapes::Plane(Plane(Properties { material: 3 })),
];

Everything worked as expected up until this point, but then I got to the task to implement some common calculation for the intersect and normal_at methods on any shape. The thing is, that this common calculation requires access to the common fields of any shape, in this example, it depends on the material of each shape. What I would like to do is something like this:

impl Shape for Shapes {
    fn intersect(&self, material: u32) -> String {
        let preprocessed_material = self.0.material + material;
        match self {
            Shapes::Plane(p) => p.intersect(preprocessed_material),
            Shapes::Sphere(s) => s.intersect(preprocessed_material),
        }
    }
}

This common functionality has to be implemented in the enum that wraps every type of shape. However, this leads me to another match expression where I repeat the same logic for each arm. The same happens for the normal_at method, and will happen for future methods I implement for any shape:

fn intersect(&self, material: u32) -> String {
    let shape_material = match self {
        Shapes::Plane(p) => p.0.material,
        Shapes::Sphere(s) => s.0.material,
    };

    let preprocessed_material = shape_material + material;

    match self {
        Shapes::Plane(p) => p.intersect(preprocessed_material),
        Shapes::Sphere(s) => s.intersect(preprocessed_material),
    }
}

What I did to mitigate this is adding a material() getter in the trait definition, but I just feel this isn't the best solution, since it introduces a lot of boilerplate, cause now I have to implement this getter for Plane and Shape, and the implementation for both will be the same: return self.0.material. And also I now have to implement this on the enum wrapper, and this will be another match statement that just calls the same method for each enum variant.

Is there any common pattern in Rust to avoid this? I know that if I wanted to avoid was just the code duplication and writing getters I could use a custom macro or maybe one from a crate, but I really feel there must be a way in which this kind of polymorphic problem is handled.

Here is a link to a playground with the code I'm testing.

This doesn't really look like "polymorphism" because Shapes::Sphere(s).intersect does something different from s.intersect, even though both are supposed to represent the same "shape" (a sphere).

It's hard for me to say what is supposed to happen here because the names of these methods and their types do not match up to anything realistic -- it's not clear why you're adding materials in some cases and not others, in particular. Why does the same "preprocessing" of materials not happen in the Sphere implementation of intersect?

Because of the different behaviors between Shapes and Sphere I am tempted to say that they probably shouldn't really be implementing the same trait.

Sorry my bad. I tried to simplified the example a little too much. You are right tho, I noticed what you said that really the behavior for the concrete types deviates from the behavior of the wrapper, so I'm gonna remove the trait implementation from the concrete shapes instead. Thanks for the answer.

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.