Managing multiple traits

New to Rust - having fun - and looking for some advise.

I'm implementing a 3d graphics system. The "scene" is a list of various kinds of objects (eg, Sphere, Box, Camera, Light). Then there are several modules for different sub-systems (viewing, rendering, simulation, etc). Each of these sub-systems define a trait which defined the API objects need to implement to participate in that module. For instance, the viewing sub-system defines "Viewable" which has a "draw()" function. The rendering system defined "Renderable" which has a "bounds()" function.

Ok, so each of those system have a list of objects which are registered with that system, like:
objects : Vec<Box<dyn Renderable>>

So, now I want a list of all these objects no matter which sub-system traits they implement. Something like "Sphere" will implement all of them. "Camera" will only implement some of them. And then from this list I will want to be able to introspect "which objects in this list implement the X trait"? Pseudo code for rendering might be:

r is a renderer
for each object in scene.objects {
    if object implement the trait "Renderable" {
        r.add(object); // ?? r.add(object as Renderable)??
    }
}

Questions
1: Am I thinking about this in a Rust-like way?
2: Can anyone suggest how to implement this?

I don't think this is ideal but my first thought was to simply define an is_renderable() fn in the Renderable trait (and so on for others).

The problem is that rust needs to know that every object in the list has this function, so I think every trait would need every is_trait() fn which gets tedious.

I also suspect you would actually need a single trait that everything implements, because to my knowledge you cannot have a bound like:
T: Box<dyn Foo | Bar | Baz>, perhaps all the is_trait() fns could be in the "super trait". You might even be able to make some helpful blanket implementations for common groups of traits.

This is speculative on my part, I'm hoping there are better ways and looking forward to others' answers :slight_smile: Thanks for the question.

Edit: I've read the question. Since each system only needs implementors of one trait you won't need a super trait to match the bounds. I think the is_trait() fn per trait might be enough.

My concern would be going back and forth between systems you (rust compiler) might lose sight of what traits each object implements :frowning: as actual types get forgotten when returned into trait objects (dyn Trait) - I think!

Having said all that. The answer is probably enums. Then you can match or if let() to get at only the types that impl Renderable.

enum MultiTrait {
    Renderable(Box<dyn Renderable>),
    Viewable (Box<dyn Viewable>),
    ...
}

if let Renderable(r) = object {
    // *r implements Renderable (it's boxed)
}

Yeah, you can't do T: Box<dyn Foo | Bar | Baz> but I think something like this might get the same effect:

pub trait GraphicsTrait {}
pub trait Foo: GraphicsTrait {}
pub trait Bar: GraphicsTrait {}
pub trait Baz: GraphicsTrait {}

let x: Box<dyn GraphicsTrait> = // etc.

Might not be able to guarantee much about what a GraphicsTrait implemented object can do. But everything's just in Boxes on the heap, so well suited to Vecs.

Enums would probably be easier to work with. The only long term concern on those is if one of the variants becomes large, but with everything in Boxes they'll be nice and uniform.

1 Like

Rust doesn't provide a mechanism to do this test, but it wouldn't be useful here in any case: Because they're all stored in the same container, every object here is the same type; the test would come out the same for every value. You'll need to provide some method on that type that provides the information yourself.

One way to do this is by extending @PFaas' solution some:

pub trait GraphicsTrait {
    fn as_foo(&self)->Option<&dyn Foo> { None }
    fn as_bar(&self)->Option<&dyn Bar> { None }
    fn as_baz(&self)->Option<&dyn Baz> { None }

    fn mut_foo(&mut self)->Option<&mut dyn Foo> { None }
    fn mut_bar(&mut self)->Option<&mut dyn Bar> { None }
    fn mut_baz(&mut self)->Option<&mut dyn Baz> { None }
}

Now, if you have a dyn GraphicsTrait, you can access it as any of the more specific traits it supports. Unfortunately, there's no way to write a blanket impl for GraphicsTrait: You'll run into conflicting implementation errors pretty quickly. You can, however, write a macro to help:

macro_rules! impl_graphics_trait {
    ($ty:ty : $($trait:tt),* ) => {
        impl GraphicsTrait for $ty {
            $( impl_graphics_trait!{@ $trait} )*
        }
    };
    (@ Foo) => {
        fn as_foo(&self)->Option<&dyn Foo> { Some(self) }
        fn mut_foo(&mut self)->Option<&mut dyn Foo> { Some(self) }
    };
    (@ Bar) => {
        fn as_bar(&self)->Option<&dyn Bar> { Some(self) }
        fn mut_bar(&mut self)->Option<&mut dyn Bar> { Some(self) }
    };
    (@ Baz) => {
        fn as_baz(&self)->Option<&dyn Baz> { Some(self) }
        fn mut_baz(&mut self)->Option<&mut dyn baz> { Some(self) }
    };
}

With this macro in place, you can define your individual types like this:

struct OnlyFoo;

impl Foo for OnlyFoo {}
impl_graphics_trait!{ OnlyFoo: Foo }

struct FooAndBar;

impl Foo for FooAndBar {}
impl Bar for FooAndBar {}
impl_graphics_trait!{ FooAndBar: Foo, Bar }

(Playground)
NB: As written, the macro can't handle generics or being used outside the module it's defined. Both of those problems are fixable, but would have obscured the central point.

4 Likes

That comes to a very nice and usable interface!

Also, a Vec<Box<dyn GraphicsTrait>> would be able to map/filter:

let all: Vec<Box<dyn GraphicsTrait>> = /* vec of all objects */
let foos = all.iter()
   .map(|graphic_obj| graphic_obj.as_foo())
   .filter(|maybe_foo| maybe_foo.is_some())
   .map(|foo| foo.unwrap())

Is there a filter_map combo here that would make this easier...?

Well, this should work out of the box:

let foos = all.iter().filter_map(|graphic_obj| graphic_obj.as_foo());
2 Likes

probably even:
let foos = all.iter().filter_map(GraphicsTrait::as_foo);

nevermind, the &Boxes won’t work without auto-deref

For that to work, I think you'd need to have a definition like this somewhere. Otherwise, the Deref coercion to get the Box's contents won't happen.

impl GraphicsTrait for Box<dyn GraphicsTrait> {
    fn as_foo(&self)->Option<&dyn Foo> { *self.as_foo() }
    /* ... */
}

I think this would make a great addition to GitHub - rust-unofficial/patterns: A catalogue of Rust design patterns, anti-patterns and idioms