Search code examples
rusthashmapglobal-variablestraits

How to initialize a HashMap whose values are functions that return trait objects from functions that return concretes types?


I have the following Rust toy code, which is building a global HashMap with closures that return a Struct that is implementing a Trait (please excuse the silly example)

pub trait Animal {
    fn make_sound(&self) -> ();
}

struct Cow {}
impl Animal for Cow {
    fn make_sound(&self) -> () {
        println!("mooo!");
    }
}

struct Sheep {}
impl Animal for Sheep {
    fn make_sound(&self) -> () {
        println!("baaa!");
    }
}

fn make_cow() -> Cow {
    return Cow{};
}

fn make_sheep() -> Sheep {
    return Sheep{}
}

/*lazy_static! {
    static ref ANIMALKINGDOM1:HashMap<String,fn()->Box<dyn Animal>> = HashMap::from([
        (String::from("cow"),make_cow),
        (String::from("sheep"),make_sheep)
    ]);
}*/

/*lazy_static! {
    static ref ANIMALKINGDOM2:HashMap<String,fn()->Box<dyn Animal>> = HashMap::from([
        (String::from("cow"),|| Box::new(make_cow())),
        (String::from("sheep"),|| Box::new(make_sheep()))
    ]);
}*/

static CowBoxingClosure: fn() -> Box<dyn Animal> = || Box::new(make_cow());

lazy_static! {
    static ref ANIMALKINGDOM3:HashMap<String,fn()->Box<dyn Animal>> = HashMap::from([
        (String::from("cow"),CowBoxingClosure),
        (String::from("sheep"),|| Box::new(make_sheep()))
    ]);
}

Now, I understand that I can't use ANIMALKINGDOM1 case because make_sheep and make_cow return Cow and Sheep.

To solve this I tried using closures (ANIMALKINGDOM2) which just box the returned value from make_sheep and make_cow to get dyn Animal. But the compiler complains that although two closures have same type they are not the same and is suggesting to box the closures.

Why doesn't then the compiler complain in case of ANIMALKINGDOM3?


Solution

  • This creates an array. The compiler assumes the array element type is the type of the first element.

    The type of the first element is (String, <Closure: || Box::new(make_cow())>). However, because no two closures have the same type, the type of the second closure is (String, <Closure: || Box::new(make_sheep())>) which doesn't match the type of the first element.

        [
            (String::from("cow"),|| Box::new(make_cow())),
            (String::from("sheep"),|| Box::new(make_sheep()))
        ]
    

    The compiler isn't smart enough to back-propagate the type of the HashMap through HashMap::from into the array, and is therefore unable to coerce the closures into function pointers fn() -> Box<dyn Animal>.

    But with your third example, you use CowBoxingClosure which is pre-coerced into a function pointer when you declare the static. This makes the array element type (String, fn() -> Box<dyn Animal>) and the second element is coerced as expected.

    Another way to achieve what you want is to tell the compiler explicitly to coerce the closures into function pointers:

    lazy_static! {
        static ref ANIMALKINGDOM2:HashMap<String,fn()->Box<dyn Animal>> = HashMap::from([
            (String::from("cow"), || Box::new(make_cow())),
            (String::from("sheep"),|| Box::new(make_sheep()))
        ] as [(_, fn()->Box<dyn Animal>); 2]);
    }