Search code examples
rustpolymorphism

What is the rust way to programmatically instantiate concrete structs of known types?


I'd like to create new instances of structs based on a type given by the caller.

Given two structs: GoodBook and BadBook, I want to wrap those in a type specific Printer. Each book will have its own printer, ie: GoodBookPrinter or BadBookPrinter. Then I'd like a CollectionPrinter that can accept a Vec<GoodBook> or Vec<BadBook> and create a Vec<GoodBookPrinter<GoodBook>> or Vec<BadBookPrinter<BadBook>>.

The caller to CollectionPrinter will know which type of Printer and Book it needs, but I'm confused on how to implement this with traits/generics.

What is the rust way to do this using static dispatch?

My failed attempt:

struct GoodBook {
    id: u32,
}

struct BadBook {
    id: u32,
}

struct GoodBookPrinter<T> {
    // This doesn't actually need to be generic.
    // I'd like for this to be:
    // target: GoodBook
    target: T,
}

trait Printer<T> {
    // All printers should be new-able from the DefaultCollectionPrinter
    fn new(target: T) -> Self;
}

impl<T> Printer<T> for GoodBookPrinter<T> {
    // Compiler would start to complain here if GoodBookPrinter was defined as:
    // target: GoodBook
    // so I've made it generic.
    fn new(target: T) -> Self {
        Self {
            target
        }
    }
}

// The vec will hold Printer<GoodBook> or Printer<BadBook> but never both.
// T will always be a struct
// GoodBook and BadBook have nothing in common
// except an id field
struct DefaultCollectionPrinter<T> {
    printers: Vec<T>,
}

// I'm using a trait with an associated type to implement the functionality
// of DefaultCollectionPrinter, which must be wrong, but I don't know a better
// way to have DefaultCollectionPrinter store a Vec<GoodBookPrinter<GoodBook>> or
// Vec<BadBookPrinter<BadBook>>, etc...
trait CollectionStuff<T> {
    // Should work with GoodBookPrinter<GoodBook> and GoodBookPrinter<BadBook>
    type S: Printer<T>;

    // Wrap each book in its associated printer
    fn new(books: Vec<T>) -> DefaultCollectionPrinter<Self::S> {
        let mut printers: Vec<Self::S> = vec![];
        for book in books {
            let printer = Self::S::new(book);
            printers.push(printer);
        }

        DefaultCollectionPrinter::<Self::S> {
            printers
        }
    }
}

// Tell the DefaultCollectionPrinter that it will be working with
// a GoodBookPrinter<GoodBook> ... but how would it also work with
// BadBookPrinter<BadBook>?
impl<T> CollectionStuff<T> for DefaultCollectionPrinter<T> {
    // Error: the trait GoodBookPrinter<GoodBook>: Printer<T> is not satisfied
    type S = GoodBookPrinter<GoodBook>;
}

fn main() {
    // I want to be able to do something like this:
    // create a vec of books and have the DefaultCollectionPrinter
    // create a Vec<GoodBookPrinter<GoodBook>> or Vec<BadBookPrinter<BadBook>>
    // depending on the type of book given.
    let books = vec![GoodBook{ id: 1 }, GoodBook{ id: 2 }];
    let printers = DefaultCollectionPrinter::new(books).printers;
}


Solution

  • If you have a trait Foo<T>, you don't need to handle any possible T when implementing Foo. Since GoodBookPrinter only cares about GoodBook, we only need to implement that case.

    impl Printer<GoodBook> for GoodBookPrinter {
        fn new(target: GoodBook) -> Self {
            GoodBookPrinter { target }
        }
    }
    

    As for DefaultCollectionPrinter, you are going to need to use a where clause to restrict the type of the generic. Logically speaking, we want DefaultCollectionPrinter<T> to be generic over the types of printers it may use. Then to construct a it, we want a function that accepts a Vec<S>, but only if T implements Printer<S>. With this reasoning, it makes more sense for the new function to be generic on the type of book. To define the relationship, we then just need to use where T: Printer<S> to tell the compiler that we can only accept types of S that T can print.

    impl<T> DefaultCollectionPrinter<T> {
        fn new<S>(books: Vec<S>) -> Self
        where
            T: Printer<S>,
        {
            DefaultCollectionPrinter {
                printers: books.into_iter().map(T::new).collect(),
            }
        }
    }
    

    After doing that, we end up with this.

    struct GoodBook {
        id: u32,
    }
    
    struct BadBook {
        id: u32,
    }
    
    struct GoodBookPrinter {
        target: GoodBook,
    }
    
    trait Printer<T> {
        fn new(target: T) -> Self;
    }
    
    impl Printer<GoodBook> for GoodBookPrinter {
        fn new(target: GoodBook) -> Self {
            GoodBookPrinter { target }
        }
    }
    
    struct DefaultCollectionPrinter<T> {
        printers: Vec<T>,
    }
    
    impl<T> DefaultCollectionPrinter<T> {
        fn new<S>(books: Vec<S>) -> Self
        where
            T: Printer<S>,
        {
            DefaultCollectionPrinter {
                printers: books.into_iter().map(T::new).collect(),
            }
        }
    }
    
    
    // We also need to explicitly tell Rust what type of DefaultCollectionPrinter
    // we are creating.
    let books = vec![GoodBook{ id: 1 }, GoodBook{ id: 2 }];
    let printers = DefaultCollectionPrinter::<GoodBookPrinter>::new(books);
    

    Rust Playground