exocortex
exocortex

Reputation: 513

How can I reduce 2 nested match statements when each arm returns a different combination of a type with a type argument

My problem: I have a function that returns a Box<dyn Trait_1>-object. Let's say I have N=2 possible types at level 1 that implement this Trait_1 and I want both of these types accessible via a match statement. But now these 2 possible types are each generic as well (but have to implement Trait_2) and I have M=2 possible types at level 2 that I want to use. Now I can either have one match with 4 arms or 2 nested matches with 2 arms each. I have to write 4 statements that initialize this Box. This is very simple. But if I had N=15 types at level 1 (that impl Trait_1) and 20 types at level 2 that impl some other Trait_2 I'll end up writing 300 different initializations! That's a lot of work and also I will probably put in some errors.

I've written up a simple example of my problem with 2 nested match statements with each having 2 arms resulting in a total of 4 initializations (I've omitted the Trait-implementations)

pub trait Trait_1 {}
pub trait Trait_2 {}

pub struct SingleContainer<T: Trait_2> {
    item: T,
}

pub struct DoubleContainer<T: Trait_2> {
    item_1: T,
    item_2: T,
}

// -> impl Trait_1 for SingleContainer and DoubleContainer

struct X {
    number: f64,
}

struct Y {
    number: u32,
}

// -> impl Trait_2 for X and Y

fn main() {
    let a = // determined at runtime
    let b = // determined at runtime

    let thing = give_thing(a, b);
}

fn give_thing(thing_number: usize, thing_name: &str) -> Box<dyn Trait_1> {
    match thing_number {
        1 => match thing_name {
            "X" => Box::new(SingleContainer::<X>::default()),
            "Y" => Box::new(SingleContainer::<Y>::default()),
            _ => todo!(),
        },
        2 => match thing_name {
            "X" => Box::new(DoubleContainer::<X>::default()),
            "Y" => Box::new(DoubleContainer::<Y>::default()),
            _ => todo!(),
        },
        _ => todo!(),
    }
}

Is there a better way to do this? I have difficulties seeing another way, as each of these things is a different type. I want that at compile time each of the possible combinations of (SingleContainer, DoubleContainer) and (X,Y) "exists" and therefor will be optimized. If there's a better way to achieve this please let me know! I basically only need a function that returns a Box<dyn MyTrait>-object that gets 2 arguments. But as each combination of arguments returns a different type I'm very confused!

Edit: Would blanket implementations help in this case in any way?

(If someone has a better way of phrasing my question, feel free to correct me!)

Upvotes: 1

Views: 69

Answers (1)

Chayim Friedman
Chayim Friedman

Reputation: 71545

A little type metaprogramming can help here.

trait MakeTrait1 {
    type Make<T: Trait_2 + Default + 'static>: Trait_1 + Default + 'static;
}

struct MakeSingleContainer;
impl MakeTrait1 for MakeSingleContainer {
    type Make<T: Trait_2 + Default + 'static> = SingleContainer<T>;
}

struct MakeDoubleContainer;
impl MakeTrait1 for MakeDoubleContainer {
    type Make<T: Trait_2 + Default + 'static> = DoubleContainer<T>;
}

fn make_trait1<T: MakeTrait1>(thing_name: &str) -> Box<dyn Trait_1> {
    match thing_name {
        "X" => Box::new(T::Make::<X>::default()),
        "Y" => Box::new(T::Make::<Y>::default()),
        _ => todo!(),
    }
}

fn give_thing(thing_number: usize, thing_name: &str) -> Box<dyn Trait_1> {
    match thing_number {
        1 => make_trait1::<MakeSingleContainer>(thing_name),
        2 => make_trait1::<MakeDoubleContainer>(thing_name),
        _ => todo!(),
    }
}

Playground.

We basically create a type factory that for the inner type creates the outer type. Then we have a function that makes a runtime decision based on this type-level factory, and all left to do is to have a function that calls this function with each factory.

Upvotes: 1

Related Questions