How do I avoid Enum + Trait pattern when a struct is not object safe?

Viewed 45

I get the implications of object safety, but I'm trying to find an idiomatic way to solve for this situation.

Say I have two structs that share common behavior and also need to derive PartialEq for comparison in another part of the program:

trait Growl:PartialEq {
    fn growl(&self);
}

#[derive(PartialEq)]
struct Pikachu;

#[derive(PartialEq)]
struct Porygon;

impl Growl for Pikachu {
    fn growl(&self) {
        println!("pika");
    }
}

impl Growl for Porygon {
    fn growl(&self) {
        println!("umm.. rawr?");
    }
}

In another struct, I want to hold a Vec of these objects. Since I can't use a trait object with Vec<Box<Growl>>...

struct Region{
    pokemon: Vec<Box<dyn Growl>>,
}

// ERROR: `Growl` cannot be made into an object

... I need to get more creative. I read this article, which suggests using an enum or changing the trait. I haven't yet explored type erasure, but it seems heavy-handed for my use case. Using an enum like this is what I've ended up doing but it feels unnecessarily complex

enum Pokemon {
    Pika(Pikachu),
    Pory(Porygon),
}

Someone coming through this code in the future now needs to understand the individual structs, the trait (which provides all functionality for the structs), and the wrapper enum type to make changes.

Is there a better solution for this pattern?

1 Answers

I read this article, which suggests using an enum or changing the trait. I haven't yet explored type erasure, but it seems heavy-handed for my use case.

Type erasure is just a synonym term for dynamic dispatch - even your original Box<dyn Growl> "erases the type" of the Pokemon. What you want here is to continue in the same vein, by creating a new trait better taylored to your use case and providing a blanket implementation of that trait for any type that implements the original trait.

It sounds complex, but it's actually very simple, much simpler than erased-serde, which has to deal with serde's behemoth traits. Let's go through it step by step. First, you create a trait that won't cause issues with dynamic dispatch:

/// Like Growl, but without PartialEq
trait Gnarl {
    // here you'd have only the methods which are actually needed by Region::pokemon.
    // Let's assume it needs growl().
    fn growl(&self);
}

Then, provide a blanket implementation of your new Gnarl trait for all types that implement the original Growl:

impl<T> Gnarl for T
where
    T: Growl,
{
    fn growl(&self) {
        // Here in the implementation `self` is known to implement `Growl`,
        // so you can make use of the full `Growl` functionality, *including*
        // things not exposed to `Gnarl` like PartialEq
        <T as Growl>::growl(self);
    }
}

Finally, use the new trait to create type-erased pokemon:

struct Region {
    pokemon: Vec<Box<dyn Gnarl>>,
}

fn main() {
    let _region = Region {
        pokemon: vec![Box::new(Pikachu), Box::new(Porygon)],
    };
}

Playground

Related