0

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?

Jk Jensen
  • 339
  • 2
  • 4
  • 16
  • 4
    If you cut out the `PartialEq` dependency, then your trait *is* object safe. And I'm not sure why a Pokemon's ability to growl depends on its ability to compare itself to others of its kind. Is this your actual trait or a simplified version? – Silvio Mayolo Sep 19 '22 at 19:41
  • Thanks, yeah I will update the question to reflect that this is a toy example based on my real code. I need the `PartialEq` derive to exist on the trait. – Jk Jensen Sep 19 '22 at 20:51

1 Answers1

1

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

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 1
    I'm wondering if a tool -- or something in the language -- can be created to strip automatically traits to their object safe representation. –  Sep 19 '22 at 22:28
  • 2
    @IsmailMaj erased-serde [readme](https://github.com/dtolnay/erased-serde#how-it-works) hints at this possibility: "In the future maybe the Rust compiler will be able to apply this technique automatically to any trait that is not already object safe by the current rules." But I've never seen a serious proposal to implement that. – user4815162342 Sep 20 '22 at 05:58
  • This is exactly what I was looking for. A proc macro could be made to derive the stripped version of a trait, right? – Jk Jensen Sep 20 '22 at 19:21
  • 1
    @JkJensen I'm not sure it'd be easy to automate. If you really need just one method, then the transformation is pretty mechanical, but if you need more functionality from the original type, it requires the kind of code that I wouldn't expect a proc macro to be able to generate. Serde's 4 fundamental traits are an extreme example of that, but also see [this code](https://stackoverflow.com/a/64840069/1600898) for a middle-ground example. – user4815162342 Sep 20 '22 at 19:32