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;
}
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);