To provide some background, I am writing a parser that creates objects that reference the text from some &'a str
, where 'a
ties all objects to the lifetime of that string. While parsing, I do some lookups to get previously-parsed items by name, which are stored in a HashMap
that holds objects that are tied to the lifetime of the parsed string.
Previously, I was using a &str
as the identifier, which worked. Here's a simplified example of what I had previously, which successfully compiled:
use std::collections::HashMap;
struct ReturnType<'a> {
key: &'a str,
value: &'a str
}
struct App<'a> {
// Key type is &'a str
map: HashMap<&'a str, &'a str>,
}
impl<'a> App<'a> {
// Note that I don't have to specify the lifetime of `key` because it's only used for hashing;
// the return type does not (and shouldn't) capture the lifetime of `key`, since it's returning
// a value that comes from `self.map`
pub fn lookup_value(&self, key: &str) -> Option<ReturnType<'a>> {
self.map.get_key_value(key).map(|(k, v)| ReturnType { key: k.clone(), value: v })
}
}
fn lookup<'a> (app: &'a App) -> Option<ReturnType<'a>> {
// Note that I'm calling lookup_value referencing a value with a lifetime shorter than 'a
let key = String::from("te");
app.lookup_value(&key)
}
fn main() {
let root = "testing";
let app = App {
map: HashMap::from([(&root[0..2], root)]),
};
lookup(&app);
}
However, I recently replaced &'a str
in the HashMap
key with a new wrapper type, StrRef<'a>
which simply wraps around the same &'a str
that was previously used. However, now it doesn't compile:
use std::collections::HashMap;
#[derive(Clone, Hash, PartialEq, Eq)]
struct StrRef<'a> {
value: &'a str,
}
struct ReturnType<'a> {
key: StrRef<'a>,
value: &'a str
}
struct App<'a> {
// Key type is StrRef<'a>
map: HashMap<StrRef<'a>, &'a str>,
}
impl<'a> App<'a> {
// I have tried `StrRef` and `StrRef<'b>` and all have the same error
pub fn lookup_value(&self, key: &StrRef) -> Option<ReturnType<'a>> {
// Error: explicit lifetime required in the type of `key`; lifetime `'a` required
self.map.get_key_value(key).map(|(k, v)| ReturnType { key: k.clone(), value: v })
}
}
fn lookup<'a> (app: &'a App) -> Option<ReturnType<'a>> {
let key = String::from("te");
app.lookup_value(&StrRef { value: &key })
}
fn main() {
let root = "testing";
let app = App {
map: HashMap::from([(StrRef { value: &root[0..2] }, root)]),
};
lookup(&app);
}
I have tried providing key
to the method as type StrRef
and &StrRef
but it doesn't compile, with the error shown in the code snippet above:
error[E0621]: explicit lifetime required in the type of `key`
--> src/stack_overflow.rs:20:9
|
19 | pub fn lookup_value(&self, key: &StrRef) -> Option<ReturnType<'a>> {
| ------- help: add explicit lifetime `'a` to the type of `key`: `&stack_overflow::fails::StrRef<'a>`
20 | self.map.get_key_value(key).map(|(k, v)| ReturnType { key: k.clone(), value: v })
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetime `'a` required
The issue is that I am not able to assign the lifetime 'a
to StrRef
because I need to be able to provide a temporary StrRef
to the lookup (where its inner &str
composed from a format!()
-ed string). I'm confused why the compiler seems to be capturing the lifetime of StrRef
in the return type when it's only being used for hashing? I'm also confused why this works with a &str
but not with StrRef
.
The way to allow querying a map with different lifetime (or different type) is by implementing the Borrow
trait. There is a little complication that arises from the fact that there exists a blanket implementation impl<T> Borrow<T> for T
and it conflicts with naively implementing Borrow
for different lifetimes, but that can be solved by introducing a wrapper type:
use std::collections::HashMap;
#[derive(Clone, Hash, PartialEq, Eq)]
struct StrRef<'a> {
value: &'a str,
}
#[derive(Clone, Hash, PartialEq, Eq)]
struct StoredStrRef<'a>(StrRef<'a>);
impl<'a: 'b, 'b> std::borrow::Borrow<StrRef<'b>> for StoredStrRef<'a> {
fn borrow(&self) -> &StrRef<'b> {
&self.0
}
}
struct ReturnType<'a> {
key: StrRef<'a>,
value: &'a str,
}
struct App<'a> {
map: HashMap<StoredStrRef<'a>, &'a str>,
}
impl<'a> App<'a> {
pub fn lookup_value(&self, key: &StrRef<'_>) -> Option<ReturnType<'a>> {
self.map.get_key_value(key).map(|(k, v)| ReturnType {
key: k.0.clone(),
value: v,
})
}
}
fn lookup<'a>(app: &'a App) -> Option<ReturnType<'a>> {
let key = String::from("te");
app.lookup_value(&StrRef { value: &key })
}
fn main() {
let root = "testing";
let app = App {
map: HashMap::from([(StoredStrRef(StrRef { value: &root[0..2] }), root)]),
};
lookup(&app);
}