Polymorphic collections

Sorry for the long post.

This question has probably been asked, and replied, many times before, but I'm not finding a similar situation.

It's about polymorphic collections like Vec<Box<dyn Trait>>.

Let's say I have several recipes defined by the trait Recipe {} and various fruits defined by the trait Fruit {}. For each recipe, I want to prepare each fruit appropriately. My approach was to define a trait Ingredient like this:

trait Ingredient<T>
where
    T: Fruit,
{
    fn prepare(&self, t: &T);
}

And then, for each recipe and fruit combination, I implemented the Ingredient trait. For example:

impl Ingredient<Apple> for Bavarian {
    fn prepare(&self, b: &Apple) {
        println!("Preparing an Apple for Bavarian Fruit Salad -> {self:?} with an {b:?}.");
    }
}

This code works as expected:

let apple = Apple {};
let banana = Banana {};
let bavarian = Bavarian {};
bavarian.prepare(&apple);   // OK
bavarian.prepare(&banana);  // OK

What's confusing me is that the following code doesn't compile:

let poly: Vec<Box<dyn Fruit>> = vec![
    Box::new(Apple {}),
    Box::new(Banana {})];
for s in poly {
    // Next line doesn't compile
    bavarian.prepare(&s);
}

I understand that the issue is related to the fact that Box<dyn Fruit> does not implement the Fruit trait.

However, if Fruit defines a method, say eat(&self), then the following code compiles and runs fine:

for s in poly {
    s.eat(); // OK
}

Here is a (somewhat) minimal example:

use std::fmt::Debug;
// --------------------
trait Fruit: Debug {
    fn eat(&self);
}

// --------------------
trait Recipe: Debug {}

// --------------------
trait Ingredient<T>
where
    T: Fruit,
{
    fn prepare(&self, t: &T);
}

// --------------------
#[derive(Debug)]
struct Apple {}
impl Fruit for Apple {
    fn eat(&self) {
        println!("Eating an Apple!");
    }
}

// --------------------
#[derive(Debug)]
struct Banana {}
impl Fruit for Banana {
    fn eat(&self) {
        println!("Eating a Banana!");
    }
}

// --------------------
#[derive(Debug)]
struct Bavarian {}
impl Recipe for Bavarian {}

impl Ingredient<Apple> for Bavarian {
    fn prepare(&self, b: &Apple) {
        println!("Preparing an Apple for Bavarian Fruit Salad -> {self:?} with an {b:?}.");
    }
}

impl Ingredient<Banana> for Bavarian {
    fn prepare(&self, b: &Banana) {
        println!("Preparing a Banana for Bavarian Fruit Salad -> {self:?} with a {b:?}.");
    }
}

// --------------------
fn main() {
    let apple = Apple {};
    let banana = Banana {};
    let bavarian = Bavarian {};
    bavarian.prepare(&apple);   // OK
    bavarian.prepare(&banana);  // OK

    let poly: Vec<Box<dyn Fruit>> = vec![Box::new(Apple {}), Box::new(Banana {})];
    for s in poly {
        s.eat(); // OK
        bavarian.prepare(&s);   // Error
        // the trait bound `Bavarian: Ingredient<Box<dyn Fruit>>` is not satisfied
        // the trait `Ingredient<Box<dyn Fruit>>` is not implemented for `Bavarian`
    }
}

What helped me get a grasp of trait objects was to think of them as concrete types.[1] They aren't placeholders, like generic arguments. dyn Fruit is a concrete type, same as Banana or Apple.[2] Which means you can (and have to) implement Ingredient<dyn Fruit> for Bavarian, if you want to pass a dyn Fruit to Bavarian::prepare:

use std::fmt::Debug;

trait Fruit: Debug {
    fn eat(&self);
}

trait Ingredient<T>
where
    T: Fruit + ?Sized,
{
    fn prepare(&self, t: &T);
}

#[derive(Debug)]
struct Apple {}
impl Fruit for Apple {
    fn eat(&self) {
        println!("Eating an Apple!");
    }
}

#[derive(Debug)]
struct Banana {}
impl Fruit for Banana {
    fn eat(&self) {
        println!("Eating a Banana!");
    }
}

#[derive(Debug)]
struct Bavarian {}

impl Ingredient<Apple> for Bavarian {
    fn prepare(&self, b: &Apple) {
        println!("Preparing an Apple for Bavarian Fruit Salad -> {self:?} with an {b:?}.");
    }
}

impl Ingredient<Banana> for Bavarian {
    fn prepare(&self, b: &Banana) {
        println!("Preparing a Banana for Bavarian Fruit Salad -> {self:?} with a {b:?}.");
    }
}

impl Ingredient<dyn Fruit> for Bavarian {
    fn prepare(&self, b: &dyn Fruit) {
        println!("Preparing a dynamic Fruit for Bavarian Fruit Salad -> {self:?} with a {b:?}.");
    }
}

fn main() {
    let apple = Apple {};
    let banana = Banana {};
    let bavarian = Bavarian {};
    bavarian.prepare(&apple); // OK
    bavarian.prepare(&banana); // OK

    let poly: Vec<Box<dyn Fruit>> = vec![Box::new(Apple {}), Box::new(Banana {})];
    for s in poly {
        s.eat(); // OK
        bavarian.prepare(&*s); // OK
    }
}

Playground.


  1. Okay what really helped me was reading quinedot's great summary on trait objects. â†Šī¸Ž

  2. Unlike Banana and Apple, dyn Fruit is unsized, which makes it a bit harder to deal with (e.g. it needs to be behind a pointer) â†Šī¸Ž

1 Like

Thank you for the response, the hints, the example, and the references, which I will consult right away.

However, it seems to me that in impl Ingredient<dyn Fruit> for Bavarian, the automatic dispatch of the prepare method is lost.

I'm not sure I understand what automatic dispatch is lost. Would you mind providing an example for clarification?

Sure. In

bavarian.prepare(&apple);  // Uses `impl Ingredient<Apple> for Bavarian`
bavarian.prepare(&banana); // Uses `impl Ingredient<Banana> for Bavarian`

different methods are called, the first one defined in impl Ingredient<Apple> for Bavarian and the second one in impl Ingredient<Banana> for Bavarian — that can be very different. But in

for s in poly {
        ...
        bavarian.prepare(&*s); // Uses `impl Ingredient<dyn Fruit> for Bavarian` twice.
    }

only the method defined in impl Ingredient<dyn Fruit> for Bavarian is being called.

You're not going to be able to get the Ingredients implementation by only supplying fruits.

It might seem obvious to you that every fruit is an ingredient, but that isn't true.

Nowhere have you defined an implementation of Prepare for any Fruit, you can't go from dyn Fruit to "oh this dyn Fruit is an Apple and I know Apples can be prepared.

You'd have to do so with only the knowledge that it is a fruit, it could be your Apple, or it could be my Cantaloupe, how do you prepare Cantaloupe for a Bavarian!? We don't know, so it cannot be accepted by the compiler.

I think a different formation of the traits may make more sense:

In that playground I've switched it so Fruit is no longer a trait, and Ingredients are generic over the recipe.

1 Like

Yes. As I said, dyn Fruit is a concrete type, same as Banana and Apple. It is not like a generic parameter that is a stand-in for a concrete type.

You'd probably cut it into small pieces, like all the other fruits you'd put in the salad. AFAIK (Bavarian) fruit salad isn't a traditional dish with lots of rules attached. Also, you can grow Cantaloupes in Germany without a greenhouse, if you have a sufficiently sunny place in your garden. So it is not such an unlikely ingredient as you might think, especially in late summer, which is the best time to eat fruit salads anyway.

1 Like

Thank you. It seems good!

I can also setup custom "preparations" for different (no longer) "Fruits", as in:

impl Ingredient<Bavarian> for Apple {
    fn prepare(&self, recipe: &Bavarian) {
        std::println!("APPLE: Preparing a {self:?} for {recipe:?}");
    }
}

impl Ingredient<Bavarian> for Banana {
    fn prepare(&self, recipe: &Bavarian) {
        std::println!("BANANA: Preparing an {self:?} for {recipe:?}");
    }
}

What a turn of mindset!

these are statically dispatched, they are NOT dispatched at runtime through the Ingredient trait. except for the syntax, it's no different than inherent methods like this;

impl Bavarian {
    fn prepare_apple(&self, a: &Apple) { todo!() }
    fn prepare_banana(&self, b: &Banana { todo!() }
}

bavarian.prepare_apple(&apple);
bavarian.prepare_banana(&banana);

however, the "actuall" type of dyn Fruit or Box<dyn Fruit> is erased at compile time, so you cannot do static dispatch over it, if you want to dispatch dynamically, you must do it indirectly using a dyn-safe trait, in other words, the "visitor" pattern.

Thank you for the static dispatch and "visitor" pattern tips :+1: