Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I avoid a ripple effect from changing a concrete struct to generic?

I have a configuration struct that looks like this:

struct Conf {
    list: Vec<String>,
}

The implementation was internally populating the list member, but now I have decided that I want to delegate that task to another object. So I have:

trait ListBuilder {
    fn build(&self, list: &mut Vec<String>);
}

struct Conf<T: Sized + ListBuilder> {
    list: Vec<String>,
    builder: T,
}

impl<T> Conf<T>
where
    T: Sized + ListBuilder,
{
    fn init(&mut self) {
        self.builder.build(&mut self.list);
    }
}

impl<T> Conf<T>
where
    T: Sized + ListBuilder,
{
    pub fn new(lb: T) -> Self {
        let mut c = Conf {
            list: vec![],
            builder: lb,
        };
        c.init();
        c
    }
}

That seems to work fine, but now everywhere that I use Conf, I have to change it:

fn do_something(c: &Conf) {
    // ...
}

becomes

fn do_something<T>(c: &Conf<T>)
where
    T: ListBuilder,
{
    // ...
}

Since I have many such functions, this conversion is painful, especially since most usages of the Conf class don't care about the ListBuilder - it's an implementation detail. I'm concerned that if I add another generic type to Conf, now I have to go back and add another generic parameter everywhere. Is there any way to avoid this?

I know that I could use a closure instead for the list builder, but I have the added constraint that my Conf struct needs to be Clone, and the actual builder implementation is more complex and has several functions and some state in the builder, which makes a closure approach unwieldy.

like image 301
mushin Avatar asked Jul 04 '17 18:07

mushin


2 Answers

While generic types can seem to "infect" the rest of your code, that's exactly why they are beneficial! The compiler knowledge about how big and specifically what type is used allow it to make better optimization decisions.

That being said, it can be annoying! If you have a small number of types that implement your trait, you can also construct an enum of those types and delegate to the child implementations:

enum MyBuilders {
    User(FromUser),
    File(FromFile),
}

impl ListBuilder for MyBuilders {
    fn build(&self, list: &mut Vec<String>) {
        use MyBuilders::*;
        match self {
            User(u) => u.build(list),
            File(f) => f.build(list),
        }
    }
}

// Support code

trait ListBuilder {
    fn build(&self, list: &mut Vec<String>);
}

struct FromUser;
impl ListBuilder for FromUser {
    fn build(&self, list: &mut Vec<String>) {}
}

struct FromFile;
impl ListBuilder for FromFile {
    fn build(&self, list: &mut Vec<String>) {}
}

Now the concrete type would be Conf<MyBuilders>, which you can use a type alias to hide.

I've used this to good effect when I wanted to be able to inject test implementations into code during testing, but had a fixed set of implementations that were used in the production code.

The enum_dispatch crate helps construct this pattern.

like image 147
Shepmaster Avatar answered Nov 01 '22 03:11

Shepmaster


You can use the trait object Box<dyn ListBuilder> to hide the type of the builder. Some of the consequences are dynamic dispatch (calls to the build method will go through a virtual function table), additional memory allocation (boxed trait object), and some restrictions on the trait ListBuilder.

trait ListBuilder {
    fn build(&self, list: &mut Vec<String>);
}

struct Conf {
    list: Vec<String>,
    builder: Box<dyn ListBuilder>,
}

impl Conf {
    fn init(&mut self) {
        self.builder.build(&mut self.list);
    }
}

impl Conf {
    pub fn new<T: ListBuilder + 'static>(lb: T) -> Self {
        let mut c = Conf {
            list: vec![],
            builder: Box::new(lb),
        };
        c.init();
        c
    }
}
like image 23
red75prime Avatar answered Nov 01 '22 04:11

red75prime