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
.