Clown
Clown

Reputation: 75

What pattern to utilize to use a Vec of differing nested generic types/trait objects?

I'm trying to implement a pattern where different Processors can dictate the input type they take and produce a unified output (currently a fixed type, but I'd like to get it generic once this current implementation is working).

Below is a minimal example:

use std::convert::From;

use processor::NoOpProcessor;

use self::{
    input::{Input, InputStore},
    output::UnifiedOutput,
    processor::{MultiplierProcessor, Processor, StringProcessor},
};

mod input {
    use std::collections::HashMap;

    #[derive(Debug)]
    pub struct Input<T>(pub T);

    #[derive(Default)]
    pub struct InputStore(HashMap<String, String>);

    impl InputStore {
        pub fn insert<K, V>(mut self, key: K, value: V) -> Self
        where
            K: ToString,
            V: ToString,
        {
            let key = key.to_string();
            let value = value.to_string();
            self.0.insert(key, value);

            self
        }

        pub fn get<K, V>(&self, key: K) -> Option<Input<V>>
        where
            K: ToString,
            for<'a> &'a String: Into<V>,
        {
            let key = key.to_string();
            self.0.get(&key).map(|value| Input(value.into()))
        }
    }
}

mod processor {
    use super::{input::Input, output::UnifiedOutput};

    use super::I32Input;

    pub struct NoOpProcessor;

    pub trait Processor {
        type I;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput;
    }

    impl Processor for NoOpProcessor {
        type I = I32Input;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput {
            UnifiedOutput(input.0 .0)
        }
    }

    pub struct MultiplierProcessor(pub i32);

    impl Processor for MultiplierProcessor {
        type I = I32Input;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput {
            UnifiedOutput(input.0 .0 * self.0)
        }
    }

    pub struct StringProcessor;

    impl Processor for StringProcessor {
        type I = String;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput {
            UnifiedOutput(input.0.parse().unwrap())
        }
    }
}

mod output {
    #[derive(Debug)]
    pub struct UnifiedOutput(pub i32);
}

pub fn main() {
    let input_store = InputStore::default()
        .insert("input_a", 123)
        .insert("input_b", 567)
        .insert("input_c", "789");

    let processors = {
        let mut labelled_processors = Vec::new();
        // let mut labelled_processors: Vec<LabelledProcessor<Input<>>> = Vec::new(); // What's the correct type?
        labelled_processors.push(LabelledProcessor("input_a", Box::new(NoOpProcessor)));
        labelled_processors.push(LabelledProcessor(
            "input_b",
            Box::new(MultiplierProcessor(3)),
        ));
        // labelled_processors.push(LabelledProcessor("input_c", Box::new(StringProcessor)));

        labelled_processors
    };

    for processor in processors {
        let output = retrieve_input_and_process(&input_store, processor);
        println!("{:?}", output);
    }
}

#[derive(Debug)]
pub struct I32Input(pub i32);

impl From<&String> for I32Input {
    fn from(s: &String) -> Self {
        Self(s.parse().unwrap())
    }
}

struct LabelledProcessor<I>(&'static str, Box<dyn Processor<I = I>>)
where
    for<'a> &'a String: Into<I>;

fn retrieve_input_and_process<T>(
    store: &InputStore,
    processor: LabelledProcessor<T>,
) -> UnifiedOutput
where
    for<'a> &'a String: Into<T>,
{
    let input = store.get(processor.0).unwrap();
    processor.1.process(&input)
}

When // labelled_processors.push(LabelledProcessor("input_c", Box::new(StringProcessor))); is uncommented, I get the below compilation error:

error[E0271]: type mismatch resolving `<attempt2::processor::StringProcessor as attempt2::processor::Processor>::I == attempt2::I32Input`
   --> src/attempt2.rs:101:63
    |
101 |         labelled_processors.push(LabelledProcessor("input_c", Box::new(StringProcessor)));
    |                                                               ^^^^^^^^^^^^^^^^^^^^^^^^^ type mismatch resolving `<attempt2::processor::StringProcessor as attempt2::processor::Processor>::I == attempt2::I32Input`
    |
note: expected this to be `attempt2::I32Input`
   --> src/attempt2.rs:75:18
    |
75  |         type I = String;
    |                  ^^^^^^
    = note: required for the cast from `attempt2::processor::StringProcessor` to the object type `dyn attempt2::processor::Processor<I = attempt2::I32Input>`

I think I've learnt enough to "get" what the issue is - the labelled_processors vec expects all its items to have the same type. My problem is I'm unsure how to rectify this. I've tried to leverage dynamic dispatch more (for example changing LabelledProcessor to struct LabelledProcessor(&'static str, Box<dyn Processor<dyn Input>>);). However these changes spiral to their own issues with the type system too.

Other answers I've found online generally don't address this level of complexity with respect to the nested generics/traits - stopping at 1 level with the answer being let vec_x: Vec<Box<dyn SomeTrait>> .... This makes me wonder if there's an obvious answer that can be reached that I've just missed or if there's a whole different pattern I should be employing instead to achieve this goal?

I'm aware of potentially utilizing enums as wel, but that would mean all usecases would need to be captured within this module and it may not be able to define inputs/outputs/processors in external modules.

A bit lost at this point.

--- EDIT ---

Some extra points:

Upvotes: 0

Views: 64

Answers (1)

Finomnis
Finomnis

Reputation: 22601

One possible solution would be to make retrieve_input_and_process a method of LabelledProcessor, and then hide the type behind a trait:

use std::convert::From;

use processor::NoOpProcessor;

use self::{
    input::InputStore,
    output::UnifiedOutput,
    processor::{MultiplierProcessor, Processor, StringProcessor},
};

mod input {
    use std::collections::HashMap;

    #[derive(Debug)]
    pub struct Input<T>(pub T);

    #[derive(Default)]
    pub struct InputStore(HashMap<String, String>);

    impl InputStore {
        pub fn insert<K, V>(mut self, key: K, value: V) -> Self
        where
            K: ToString,
            V: ToString,
        {
            let key = key.to_string();
            let value = value.to_string();
            self.0.insert(key, value);

            self
        }

        pub fn get<K, V>(&self, key: K) -> Option<Input<V>>
        where
            K: ToString,
            for<'a> &'a str: Into<V>,
        {
            let key = key.to_string();
            self.0.get(&key).map(|value| Input(value.as_str().into()))
        }
    }
}

mod processor {
    use super::{input::Input, output::UnifiedOutput};

    use super::I32Input;

    pub struct NoOpProcessor;

    pub trait Processor {
        type I;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput;
    }

    impl Processor for NoOpProcessor {
        type I = I32Input;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput {
            UnifiedOutput(input.0 .0)
        }
    }

    pub struct MultiplierProcessor(pub i32);

    impl Processor for MultiplierProcessor {
        type I = I32Input;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput {
            UnifiedOutput(input.0 .0 * self.0)
        }
    }

    pub struct StringProcessor;

    impl Processor for StringProcessor {
        type I = String;
        fn process(&self, input: &Input<Self::I>) -> UnifiedOutput {
            UnifiedOutput(input.0.parse().unwrap())
        }
    }
}

mod output {
    #[derive(Debug)]
    pub struct UnifiedOutput(pub i32);
}

pub fn main() {
    let input_store = InputStore::default()
        .insert("input_a", 123)
        .insert("input_b", 567)
        .insert("input_c", "789");

    let processors = {
        let mut labelled_processors: Vec<Box<dyn LabelledProcessorRef>> = Vec::new();

        labelled_processors.push(Box::new(LabelledProcessor(
            "input_a",
            Box::new(NoOpProcessor),
        )));
        labelled_processors.push(Box::new(LabelledProcessor(
            "input_b",
            Box::new(MultiplierProcessor(3)),
        )));
        labelled_processors.push(Box::new(LabelledProcessor(
            "input_c",
            Box::new(StringProcessor),
        )));

        labelled_processors
    };

    for processor in processors {
        let output = processor.retrieve_input_and_process(&input_store);
        println!("{:?}", output);
    }
}

#[derive(Debug)]
pub struct I32Input(pub i32);

impl From<&str> for I32Input {
    fn from(s: &str) -> Self {
        Self(s.parse().unwrap())
    }
}

struct LabelledProcessor<I>(&'static str, Box<dyn Processor<I = I>>);

impl<I> LabelledProcessorRef for LabelledProcessor<I>
where
    for<'a> &'a str: Into<I>,
{
    fn retrieve_input_and_process(&self, store: &InputStore) -> UnifiedOutput {
        let input = store.get(self.0).unwrap();
        self.1.process(&input)
    }
}

trait LabelledProcessorRef {
    fn retrieve_input_and_process(&self, store: &InputStore) -> UnifiedOutput;
}
UnifiedOutput(123)
UnifiedOutput(1701)
UnifiedOutput(789)

Upvotes: 1

Related Questions