rustborrow-checker

Function that returns a value in a HashMap using a parameter requires a lifetime for Struct<'a> but not for &str


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.


Solution

  • 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);
    }
    

    Playground.