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.