Struggling with trait objects, I believe due to "Java brain"

Hello Rustaceans! I've been getting the feeling that I'm painting myself into a corner with a trait that I've written, I believe I've gotten in to trouble by thinking of traits as I would an "interface" in Java. Here is the trait in question (modified for brevity):

pub trait Shape: Sized {
    fn intersections_with<'s, 'r>(&'s self, ray: &'r Ray) -> Vec<Intersection<Self>>
    where
        'r: 's;
}

In case it's helpful, here is what my Intersection looks like:

pub struct Intersection<'o, 'r, S> {
    time: f32,
    object: &'o S,
    ray: &'r Ray,
}

impl<'o, 'r, S> Intersection<'o, 'r, S>
where
    S: Shape,
{
    ...

What I would like to do is have a Vec of structs that implement Shape (currently I have Sphere and Plane), and then be able to iterate over those things and call the intersections_with function on each of them given a single Ray. However, because the intersections_with function returns Vec<Intersection<Self>>, I'm not able to use dyn Shape.

At the moment, I'm at a loss as to how I should continue. Am I trying to go in a direction I shouldn't be, or am I missing something else going on here? I'm aiming to achieve some polymorphism here, which looks to be a fairly common stumbling point for Rust beginners—my apologies if it's too similar to other questions asked!

Thanks very much in advance for your attention!

If you remove the Sized supertrait then you can return a Vec of Intersection<&dyn Shape>>. Whether that will hold up when you add more, I'm not sure, and maybe that's where the Java brain issue comes up.

struct Ray;
pub trait Shape {
    fn intersections_with<'s, 'r>(
        &'s self,
        ray: &'r Ray,
    ) -> Vec<Intersection<'s, 'r, &dyn Shape>>
    where
        'r: 's;
}
pub struct Intersection<'o, 'r, S> {
    time: f32,
    object: &'o S,
    ray: &'r Ray,
}

impl<'o, 'r, S> Intersection<'o, 'r, S>
where
    S: Shape,
{
    //
}

I always use:

#![warn(elided_lifetimes_in_paths)]

so the lifetimes must be specified in path expressions, to avoid confusion.

You probably need to avoid &&dyn Shape for the signature to work. (And I removed the outlives bounds as there wasn't a reason for them in the example.)

If the only new information coming out of the call is time: f32, perhaps the method should return Vec<f32> (or an iterator of f32s) instead.

2 Likes

What are you doing with the intersection? What do you need it for, and what methods does it have? The usefulness of returning typed or dynamic values depends crucially on thede details.

I'm going to try to give more context by answering a few questions raised so far. @quinedot and @paramagnetic you both asked questions about the Intersection, so to start, here is an abridged version that I hope gets to the utility of the Shape associated with it:

impl<'o, 'r, S> Intersection<'o, 'r, S>
where
    S: Shape,
{
    pub fn new(time: f32, object: &'o S, ray: &'r Ray) -> Self {
        Intersection { time, object, ray }
    }

    pub fn intersected_object(&self) -> &'o S {
        self.object
    }

    pub fn normal_vector(&self) -> Vector {
        let base_normal = self.base_normal_vector();

        if self.is_inside_object() {
            -base_normal
        } else {
            base_normal
        }
    }

    fn base_normal_vector(&self) -> Vector {
        self.object.normal_at(self.point())
    }

    fn is_inside_object(&self) -> bool {
        dot(
            &-self.ray.direction().to_owned(),
            &self.base_normal_vector(),
        ) < 0f32
    }
}

@quinedot your thought about the time value possibly being the only thing necessary did get me thinking, however because there are some other ways that I'm using the Shape (kind of confusingly referred to as object), I'd like to continue to have an Intersection that holds on to these other things.

@paramagnetic does that get to your questions as well? For more context, I'm working on building a ray tracing renderer, so my conception of the Intersection is that it represents a point on a ray that intersects some sort of shape (at the moment, a sphere or a plane). Elsewhere in the code, I use the Intersection to get more information on the intersected shape, specifically the "material" of it which is then used in the calculation of the color of a pixel. Hopefully that context is helpful!

Sorry @jumpnbrownweasel, I began experimenting with your suggestion but haven't had time yet today to get very far into it! I hope I'll be back with more questions later...

NP. When/if you do, use the version that @quinedot posted when he replied to me, since it has a correction.

Hey everyone! Apologies for falling of off this, but I wanted to leave some sort of follow up because you all were very helpful, and in case this is helpful to anyone in the future. The solution that I ended up going with was to rewrite that intersections_with function on Shape to no longer return Intersections, but just a Vec of f64s that represent where the intersections occur on the given Ray—sorry I realize that that's all very specific to what I'm doing here, and not really useful knowledge.

However, a more interesting change that I made was to that Intersection, changing object from a generic type implementing Shape, to a Rc<dyn Shape>. This is really what I was trying to get at. The Rc<dyn Shape> does give me the polymorphism I was looking for, as well as some other properties that I couldn't achieve with a Box or Arc, although I'm having trouble piecing together the specifics of why those other two didn't work!

Again, thanks very much, and my apologies for not following up earlier!

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.