Reputation: 1905
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
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