The rabbit hole of `HashMap<[String; 2], V>`

⚓ Rust    📅 2025-06-25    👤 surdeus    👁️ 6      

surdeus

Warning

This post was published 47 days ago. The information described in this article may have changed.

I thought HashMap<[String; 2], V> would just work, until I had to get() a value with a [&str; 2] key. Obviously, this wouldn't work. A less stubborn person would just call .to_string() and get done with it, but I figured that maybe I could implement Borrow and make it work.

Thus, my second attempt looked like this:

struct Pair([String; 2]);

impl<'a> Borrow<[&'a str; 2]> for Pair {
    fn borrow(&self) -> &[&'a str; 2] {
        todo!()
    }
}

The whole thing compiled, I could .get() with a [&str; 2], but it became obvious there was no possible implementation for that todo!(). There was no such [&'a str; 2] for me to borrow from &self.

Then, I came up with this third attempt (a small compromise to not have to call .to_string() on the key, I would use one more byte per storage with the enum discriminant):

use std::collections::HashMap;
use std::ops::Index;

#[derive(Eq)]
enum Pair<'a> {
    Owned([String; 2]),
    Borrowed([&'a str; 2]),
}

impl PartialEq for Pair<'_> {
    fn eq(&self, other: &Self) -> bool {
        self[0] == other[0] && self[1] == other[1]
    }
}

impl std::hash::Hash for Pair<'_> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self[0].hash(state);
        self[1].hash(state);
    }
}

impl Index<usize> for Pair<'_> {
    type Output = str;

    fn index(&self, idx: usize) -> &str {
        match self {
            Pair::Owned(arr) => arr[idx].as_str(),
            Pair::Borrowed(arr) => arr[idx],
        }
    }
}

// This is a simplified version of how I was using the HashMap.
fn do_something_and_return_key_ref<'a>(map: &'a HashMap<Pair<'static>, i32>, key_a: &str, key_b: &str) -> [&'a str; 2] {
    let (key, value) = map.get_key_value(&Pair::Borrowed([key_a, key_b])).unwrap();
    // [...] do something with value and key
    [&key[0], &key[1]]
}

I thought I had nailed it. Using .get_key_value() would ensure the returned key had a lifetime that matched &map, so it would be fine. Well, unfortunately this doesn't compile:

error[E0621]: explicit lifetime required in the type of `key_a`
  --> src/lib.rs:38:5
   |
35 | fn do_something_and_return_key_ref<'a>(map: &'a HashMap<Pair, i32>, key_a: &str, key_b: &str) -> [&'a str; 2] {
   |                                                                            ---- help: add explicit lifetime `'a` to the type of `key_a`: `&'a str`
...
38 |     [&key[0], &key[1]]
   |     ^^^^^^^^^^^^^^^^^^ lifetime `'a` required

error[E0621]: explicit lifetime required in the type of `key_b`
  --> src/lib.rs:38:5
   |
35 | fn do_something_and_return_key_ref<'a>(map: &'a HashMap<Pair, i32>, key_a: &str, key_b: &str) -> [&'a str; 2] {
   |                                                                                         ---- help: add explicit lifetime `'a` to the type of `key_b`: `&'a str`
...
38 |     [&key[0], &key[1]]
   |     ^^^^^^^^^^^^^^^^^^ lifetime `'a` required

For more information about this error, try `rustc --explain E0621`.

At this point I started wondering if get_key_value() had a bug, because this is one of the explicitly cited uses of the function:

This is potentially useful:

  • [...]
  • for getting a reference to a key with the same lifetime as the collection.

The error message didn't help much either, because it didn't explain why lifetime 'a was required.

Fortunately, I already knew about the role of Borrow, and upon close inspection, I came up with this fourth attempt:

use std::borrow::Borrow;

impl<'a> Borrow<Pair<'a>> for Pair<'static> {
    fn borrow(&self) -> &Pair<'a> {
        self
    }
}

Which, of course, didn't work due to:

error[E0119]: conflicting implementations of trait `Borrow<Pair<'static>>` for type `Pair<'static>`
  --> src/lib.rs:36:1
   |
36 | impl<'a> Borrow<Pair<'a>> for Pair<'static> {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T> Borrow<T> for T
             where T: ?Sized;

For more information about this error, try `rustc --explain E0119`.

but it led me to the final working solution:

use std::borrow::Borrow;

#[derive(PartialEq, Eq, Hash)]
struct KeyPair(Pair<'static>);

impl<'a> Borrow<Pair<'a>> for KeyPair {
    fn borrow(&self) -> &Pair<'a> {
        &self.0
    }
}

// This is a simplified version of how I was using the HashMap.
fn do_something_and_return_key_ref<'a>(map: &'a HashMap<KeyPair, i32>, key_a: &str, key_b: &str) -> [&'a str; 2] {
    let (key, value) = map.get_key_value(&Pair::Borrowed([key_a, key_b])).unwrap();
    // [...] do something with value and key
    [&key.0[0], &key.0[1]]
}

In the end, I think Borrow is not the best trait to use to compare the HashMap key, because it requires a reference to be returned as the object for comparison. It would be more flexible if the trait allowed the return of any arbitrary object, which I think would also cover all the cases covered by Borrow. E.g.

pub trait ComparesWith<T> {
    fn get_comparer(&self) -> T;
}

// I didn't try to compile, but it looks like it can cover all cases where Borrow is implemented:
impl <Borrowed, T: Borrow<Borrowed>> CompareWith<&Borrowed> for T {
    fn get_comparer(&self) -> &Borrowed {
        self.borrow()
    }
}

Such a trait would have allowed something closer to my second attempt to work, because the object returned wouldn't need to be a reference taken from &self.

3 posts - 3 participants

Read full topic

🏷️ rust_feed