Shouldn't this HashMap snippet compile?

use std::borrow::Cow;
use std::collections::HashMap;

fn do_stuff<'m, 'k>(m: &'m HashMap<&'static str, String>, k: &'k str) -> Option<&'m str> {
    m.get(&k).map(String::as_str)
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
warning: unused import: `std::borrow::Cow`
 --> src/lib.rs:1:5
  |
1 | use std::borrow::Cow;
  |     ^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

error[E0623]: lifetime mismatch
 --> src/lib.rs:5:5
  |
4 | fn do_stuff<'m, 'k>(m: &'m HashMap<&'static str, String>, k: &'k str) -> Option<&'m str> {
  |                                                              -------     ---------------
  |                                                              |
  |                                                              this parameter and the return type are declared with different lifetimes...
5 |     m.get(&k).map(String::as_str)
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...but data from `k` is returned here

error: aborting due to previous error; 1 warning emitted

For more information about this error, try `rustc --explain E0623`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

Surely no data from k is returned, contrary to the error text, because the keys and values in a HashMap are separate?

Don't you need to tell the borrowchecker that k is a subset of m?

fn do_stuff<'m, 'k: 'm>(m: &'m HashMap<&'static str, String>, k: &'k str) -> Option<&'m str> {
    m.get(&k).map(String::as_str)
}

Heh, what an interesting one :upside_down_face:

The idea is that there is so much inference and lifetime adjusting going on with your call that the error ends up being a bit silly.

Without further ado, let's debug this one, shall we?

The trick is thus to try and get to annotate the types involved as much as possible; let's focus on the HashMap::get() signature:

impl<Key, Value> HashMap<Key, Value>
where
    Key : Hash + Eq,
{
    fn get (self: &'_ HashMap<Key, Value>, key: &'_ KeyView)
      -> Option<&'_ Value>
    where
        Key : /* can be */ Borrow/*-ed as a */ <KeyView>,
        KeyView : Hash + Eq,
  • (from the std lib, but renamed for clarity)

So let's try and perform an explicit call with as little elision as possible:

- m.get(&k)
+ HashMap::<&'static str, String>::get::<&'k str>(m, &k)

This doesn't change the error message, so I suspect the two-time generics are making it so the second-batch of generics (the ::<&'k str>) is actually performing a subtyping "coercion" on the Key and Value types of the HashMap; and the only kind of subtyping relationships that Rust has involve lifetimes, that means the &'static str may be subtyped to become a &'k str, for instance, since that may accomodate for some constraint we have around, and if that happens then indeed the yielded value will only be valid for the 'k lifetime, which is a lifetime which originates from k, hence the error message.

To confirm this theory, let's make sure no such subtyping takes place, by using only one layer of generics / turbofish:


fn hashmap_get<'map, Key, Value, KeyView : ?Sized>(
    map: &'map HashMap<Key, Value>,
    key: &'_ KeyView,
) -> Option<&'map Value>
where
    Key : Hash + Eq,
    Key : Borrow<KeyView>,
    KeyView : Hash + Eq,
{
    map.get(key)
}

and now:

- HashMap::<&'static str, String>::get::<&'k str>(m, &k)
+ hashmap_get::<&'static str, String, &'k str>(m, &k)

this yields:

error[E0308]: mismatched types
  --> src/lib.rs:20:5
   |
20 |     hashmap_get::<&'static str, String, &'k str>(m, &k)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetime mismatch
   |
   = note: expected trait `Borrow<&'k str>`
              found trait `Borrow<&'static str>`
note: the lifetime `'k` as defined on the function body at 18:17...
  --> src/lib.rs:18:17
   |
18 | fn do_stuff<'m, 'k>(m: &'m HashMap<&'static str, String>, k: &'k str) -> Option<&'m str> {
   |                 ^^
   = note: ...does not necessarily outlive the static lifetime

which is already hinting at a specially interesting thing, now: an unmet Borrow bound!

Indeed, remember, the get function requires that it be given a

    key: &KeyView,
where
    Key : /* can be */ Borrow /* -ed as a */ <KeyView>,

In our case, we do have Key = &'static str, and depending on how we call the .get() function, we are free to choose which KeyView type to use.
When writing &k, we have:

  • &k : &'_ (&'k str),

  • key = &k,

  • and we have key: &'_ KeyView.

Thus, KeyView = &'k str. Do we uphold Key : Borrow<Keyview>?

i.e., do we have:

&'static str : Borrow<&'k str>

? No, we don't. Hence the error. The only impl that may apply here is the "reflexivity of Borrow" rule, i.e., something may be borrowed as itself, which for Self = &'static str, yields
&'static str : Borrow<&'static str>.

There is also another rule, which states that a reference can be borrowed as its referee. When the referee is str, this gives:

for<'any>
    &'any str : Borrow<str>
,

Thus, if you had given k rather than &k, you'd have had KeyView = str rather than KeyView = &'k str and you'd have had no trait error [and thus no weird subtyping "coercion"] and thus no lifetime error!

Hence the fix:

- m.get(&k)…
+ m.get( k)…

The really interesting thing here, regarding Rust, is that when the language should have encountered an unresolved trait bound, instead, it still tried very hard to make compilation pass, by "abusing a legal loophole", we could say :sweat_smile:, regarding subtyping of the Key type. That is, when Rust encountered a:

// Inferred; can still be subtyped / the lifetime can shrink
// vvvvvvvvvvvv
   &'static str :? Borrow<&'k str>

Rust decided that rather than bailing out directly, if it were to shrink that 'static down to 'k, it could make the bound pass. It thus dodged that compilation error (:clap:) but in doing so, it just threw itself into the lion borrow checker's mouth, which yielded a way more obtuse error message :sweat_smile:


Rust may be an incredibly smart automaton, but it's not able to solve the trolley problem (yet) :upside_down_face:

image

7 Likes

If you change m.get(&k) to m.get(k) then it works (Playground).

The HashMap::get method requires K: Borrow<Q>.

In the original code, we have Q = &'k str and K = &'static str. We need &'static str: Borrow<&'k str>, which is only true when 'k == 'static (using the blanket impl<T> Borrow<T> for T>).

In my revised version, we have Q = str, so we need &'static str: Borrow<str>, which is always true because of the blanket impl<T> Borrow<T> for &T.

The compiler's error message seems misleading and should be improved.

6 Likes

Thank you all for the responses,. I know now that the example I posted is too far from my real code, but for the time being, @blonk's solution works for me. I don't know why.

In my real code I want the keys to be a tuple of strings, so that is HashMap<(Cow<'static, str>, Cow<'static, str>), String> (because I want the keys to be owned in the map, but borrowed during lookup - not actually necessary but I thought it would be an easy optimisation). So, turning get(&key) into get(key) doesn't work, because key isn't a reference and get takes a reference.

My real code is here, and it fails with the same error message as in my original post:

pub fn get<'s, 'k: 's>(&'s self, (event_type, state_key): (&'k str, &'k str)) -> Option<&'s str> {
    let key = (Cow::from(event_type), Cow::from(state_key));
    self.map.get(&key).map(String::as_str)
}

This works since changing 'k to 'k: 's, but I don't understand why the key has to outlive the map when it is only used during lookup. Or am I looking at this the wrong way, and is there a better way to approach this tuple key pattern?

Edit: OK now I'm confused. At one of the call sites of this function, 'k totally doesn't outlive 's and it still compiles. I guess I just don't understand this at all

Actually due to variance m can be coerced to &'m HashMap<&'m str, String> which makes K=&'m str, which still requires 'k: 'm to coerce k into &'m str.

This feels like Lifetime of input (key) to HashMap::get() gets entanged w/ that of its output (value) · Issue #80389 · rust-lang/rust · GitHub

4 Likes

Note from experience: adding "outlives" bounds is very rarely the correct thing. For instance, in those signatures, adding a 'k ⊇ 'm is basically equivalent to taking 'k = 'm altogether, so you are still having the output tied to the lifetime of just a lookup key ⟹ not a real solution! :warning:


Sadly, Cow neat semantics don't compose well with tuples (structural composition) by default; you need to write a Borrow impl yourself to hint it in the right direction, which thus means you need a newtype:

#[derive(Hash, PartialEq, Eq)]
pub
struct KeyView<'key> {
    pub event_type: Cow<'key, str>,
    pub state_key: Cow<'key, str>,
}

/* Sadly we can't just do:
type Key = KeyView<'static>;
// Because of coherence / overlapping impls:
// we need yet another new type -__-' : */
#[derive(Hash, PartialEq, Eq)]
pub
struct Key /* = */ (
    pub KeyView<'static>,
);

impl<'key>
 // can be
    Borrow/*-ed as a */ <KeyView<'key>>
for
    Key
{
    fn borrow<'borrow> (self: &'borrow Key)
      -> &'borrow KeyView<'key>
    {
        // Since these borrows are read-only,
        // lifetimes can Just Shrink™ (covariance)
        &self.0
    }
}

pub
fn get<'map> (
    map: &'map ::std::collections::HashMap<Key, String>,
    (event_type, state_key): (&'_ str, &'_ str),
) -> Option<&'map str>
{
    let key_view = KeyView {
        event_type: Cow::Borrowed(event_type),
        state_key: Cow::Borrowed(state_key),
    };
    map.get(&key_view).map(String::as_str)
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.