1

I have different structs which all contain a HashMap with String as the key, but with different value types. For example, one struct has a member of type HashMap<String, String>, the other will have a member of type HashMap<String, u8>, and so on.

I want to define a method that can access these HashMap members and do generic actions on them that don't involve the value. For example, I want to count the number of keys, remove a key, check if a key exists, etc. I'm not sure how to implement this behavior.

The best way I had in mind so far is to define a trait that has a method that exposes the HashMap and let each struct implement it. However I don't know how to write this trait and method in a way that "ignores" the value type. I tried using a wildcard (_) but it doesn't work. How do I implement this?

Here is my code (which doesn't compile):

use std::collections::HashMap;

pub trait HashMapContainer {
    fn get_hash_map(&self) -> HashMap<String, _>;
}

struct HashMapContainerImpl1 {
    map: HashMap<String, String>,
}

impl HashMapContainerImpl1 {
    pub fn new() -> HashMapContainerImpl1 {
        HashMapContainerImpl1 {
            map: HashMap::new(),
        }
    }

    fn internal_logic_on_map(&mut self) {
        //....
    }
}

impl HashMapContainer for HashMapContainerImpl1 {
    fn get_hash_map(&self) -> HashMap<String, _> {
        self.map
    }
}

struct HashMapContainerImpl2 {
    map: HashMap<String, u8>,
}

impl HashMapContainerImpl2 {
    pub fn new() -> HashMapContainerImpl2 {
        HashMapContainerImpl2 {
            map: HashMap::new(),
        }
    }

    fn internal_logic_on_map(&mut self) {
        //....
    }
}

impl HashMapContainer for HashMapContainerImpl2 {
    fn get_hash_map(&self) -> HashMap<String, _> {
        self.map
    }
}

fn do_generic_actions_on_map(hm_container: &HashMapContainer) {
    println!("key count: {}", hm_container.get_hash_map().len());
    println!(
        "key esists? {}",
        hm_container.get_hash_map().get("key1").is_some()
    );
    hm_container.get_hash_map().remove("key2");
}

fn main() {
    let cont1 = HashMapContainerImpl1::new();
    let cont2 = HashMapContainerImpl2::new();
    do_generic_actions_on_map(cont1);
    do_generic_actions_on_map(cont2);
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
seladb
  • 852
  • 1
  • 13
  • 29

1 Answers1

2

With an associated type

The code below using a generic is correct, but after thinking about it, I think that using an associated type might be more suitable here. The trait should look like this:

pub trait HashMapContainer {
    type Value;
    fn get_hash_map(&self) -> &HashMap<String, Self::Value>;
    fn get_hash_map_mut(&mut self) -> &mut HashMap<String, Self::Value>;
}

The difference is, that you now can only implement the trait once for a struct and not multiple times, which is more correct in this case.

The implementations are roughly the same as with the generic type parameter.

impl HashMapContainer for HashMapContainerImpl1 {
    type Value = String;
    fn get_hash_map(&self) -> &HashMap<String, Self::Value> {
        &self.map
    }
    fn get_hash_map_mut(&mut self) -> &mut HashMap<String, Self::Value> {
        &mut self.map
    }
}

impl HashMapContainer for HashMapContainerImpl2 {
    type Value = u8;
    fn get_hash_map(&self) -> &HashMap<String, Self::Value> {
        &self.map
    }
    fn get_hash_map_mut(&mut self) -> &mut HashMap<String, Self::Value> {
        &mut self.map
    }
}

(Playground)

You can also look at When is it appropriate to use an associated type versus a generic type? which explains the difference between those two pretty good.

With a generic type

This is solvable by introducing a generic type in your HashMapContainer trait.

pub trait HashMapContainer<T> {
    fn get_hash_map(&self) -> &HashMap<String, T>;
    fn get_hash_map_mut(&mut self) -> &mut HashMap<String, T>;
}

I changed the signature to return a reference to the HashMap. It would be possible to do it without, e.g. by using clone or taking self as value, instead of as reference. I also introduced a _mut version.

The implementation is straight foreword:

impl HashMapContainer<String> for HashMapContainerImpl1 {
    fn get_hash_map(&self) -> &HashMap<String, String> {
        &self.map
    }
    fn get_hash_map_mut(&mut self) -> &mut HashMap<String, String> {
        &mut self.map
    }
}

impl HashMapContainer<u8> for HashMapContainerImpl2 {
    fn get_hash_map(&self) -> &HashMap<String, u8> {
        &self.map
    }
    fn get_hash_map_mut(&mut self) -> &mut HashMap<String, u8> {
        &mut self.map
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
hellow
  • 12,430
  • 7
  • 56
  • 79
  • I have a follow up question: suppose I want to use `HashMapContainer` as a member in another struct (let's call it `MyDB`) and in `MyDB` constructor I want to decide whether to construct this member as `HashMapContainerImpl1` or `HashMapContainerImpl2`. I don't want to define `MyDB` as a template (e.g `MyDB`) because `MyDB` users don't care about the value of the HashMap, `MyDB` constructor will decide about that. What is the right way to implement that? – seladb Jan 17 '19 at 10:06
  • Use a enum with two variants, one for `Impl1` and one for `Impl2`. For the future, please ask a new question and refer to this one :) – hellow Jan 17 '19 at 10:11
  • I just did, thanks for this comment: https://stackoverflow.com/questions/54233898/struct-members-who-are-traits-that-use-associated-types. Could you please provide a more detailed answer there? – seladb Jan 17 '19 at 10:30