Vladimir Lagunov
Vladimir Lagunov

Reputation: 1905

Generic HashMap does not implement method get

I am trying to create generic structure that will contain lots of generic typed fields. AST of this structure will be generated by the compiler plugin and will be used for rendering text from template.

use std::collections::HashMap;
use std::string::ToString;


pub struct Context<Key, Value>
    where Value: ToString
{
    value: HashMap<Key, Value>,
    // here will be other fields with different generic types
}


impl <Key, Value> Context <Key, Value>
    where Value: ToString
{
    // In Jinja2 this can be written like
    // `some text before ... {{ value["foobar"] }} ... text after`
    pub fn render_to_string(&self) -> String {
        let mut r = String::new();
        r.push_str("text before ... ");

        // We see that the type of key is &'static str.
        // It is easy to determine when code written manually by human.
        // But such code will be generated by compiler plugin.
        // Here can be integer or another field from &self
        // instead of static string.
        self.value.get(&("foobar")).map(|v: &Value| r.push_str(&v.to_string()));

        r.push_str(" ... text after");
        r
    }
}


fn main() {
    let ctx1 = Context {
        value: {
            let mut v = HashMap::new();
            v.insert("foobar", 123u64);
            v
        },
    };
    println!("{:?}", ctx1.render_to_string());
}

Unfortunately Rust refuses to compile code with such fuzzy types, but instead of telling what traits should be defined for generics it outputs:

<anon>:25:20: 25:36 error: type `std::collections::hash::map::HashMap<Key, Value>` does not implement any method in scope named `get`
<anon>:25         self.value.get(&("foobar")).map(|v: &Value| r.push_str(&v.to_string()));
                             ^~~~~~~~~~~~~~~~

Can this code will be fixed without specifying exact types of Key and Value?

Upvotes: 3

Views: 3178

Answers (1)

Shepmaster
Shepmaster

Reputation: 431689

Let's check out the type signature of get:

impl<K, V, S> HashMap<K, V, S>
    where K: Eq + Hash,
          S: HashState
{
    fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> 
        where K: Borrow<Q>,
              Q: Hash + Eq
    {}
}

All of those conditions need to be true for the get method to be available. Specifically, in your case, you need for the Key to be Eq + Hash:

impl <Key, Value> Context <Key, Value>
    where Key: Eq + std::hash::Hash,
          Value: ToString

This leads to a new error, because you are using a &str as the key to get, so you really have to specify that a &str can be used as a key:

impl<Key, Value> Context <Key, Value>
    where Key: Eq + std::hash::Hash + std::borrow::Borrow<str>,
          Value: ToString

Then, remove the reference from your get line:

self.value.get("foobar")

So, path std::borrow::Borrow<str> strictly specifies that we know the real type of key at code generation stage.

It does not specify you know the real type of the key. It only requires that whatever your key is, a &str can be borrowed from a &Key. For example, your Key could be a String or a CowString. This is required because you are using "foobar" as the key.

[The key] can be integer or another field from &self instead of static string.

If it's another member of self, then you could parameterize the struct appropriately:

use std::collections::HashMap;
use std::string::ToString;
use std::hash::Hash;

pub struct Context<Key, Value> {
    value: HashMap<Key, Value>,
    key: Key,
}

impl <Key, Value> Context <Key, Value>
    where Key: Eq + Hash,
          Value: ToString,
{
    pub fn render_to_string(&self) -> String {
        let mut r = "text before ... ".to_string();

        self.value.get(&self.key).map(|v| r.push_str(&v.to_string()));

        r.push_str(" ... text after");
        r
    }
}

fn main() {
    let ctx1 = Context {
        key: 42,
        value: {
            let mut v = HashMap::new();
            v.insert(42, 123u64);
            v
        },
    };
    println!("{:?}", ctx1.render_to_string());
}

to force Rust to resolve key type at first self.value.get call

This sounds like you want dynamic (runtime) typing - which doesn't exist. Rust is a statically-typed language, so types must be determined at compile time.

If you have a fixed set of key types, you could simply create an enum:

enum Key {
    String(String),
    Int(i32),
}

And then use that instead of a generic.

Upvotes: 3

Related Questions