web-dev-qa-db-fra.com

Comment implémenter HashMap avec deux clés?

HashMap implémente les méthodes get et insert qui prennent un seul emprunt immuable et un seul mouvement d'une valeur, respectivement.

Je veux un trait qui est juste comme ça mais qui prend deux clés au lieu d'une. Il utilise la carte à l'intérieur, mais c'est juste un détail de mise en œuvre.

pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    map: HashMap<(A, B), f64>,
}

impl<A: Eq + Hash, B: Eq + Hash> Memory<A, B> for Table<A, B> {
    fn get(&self, a: &A, b: &B) -> f64 {
        let key: &(A, B) = ??;
        *self.map.get(key).unwrap()
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        self.map.insert((a, b), v);
    }
}
19
lsunsi

C'est certainement possible. La signature de get est

fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> 
where
    K: Borrow<Q>,
    Q: Hash + Eq, 

Le problème ici est d’implémenter un type &Q tel que

  1. (A, B): Borrow<Q>
  2. Q implémente Hash + Eq

Pour satisfaire la condition (1), nous devons réfléchir à la manière d'écrire

fn borrow(self: &(A, B)) -> &Q

L'astuce est que &Q n'a pas besoin d'être un simple pointeur , ce peut être un objet trait ! L'idée est de créer un trait Q qui aura deux implémentations:

impl Q for (A, B)
impl Q for (&A, &B)

L'implémentation Borrow renverra simplement self et nous pouvons construire un objet trait &Q à partir des deux éléments séparément.


Le implémentation complète est comme ceci:

use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::borrow::Borrow;

// See explanation (1).
#[derive(PartialEq, Eq, Hash)]
struct Pair<A, B>(A, B);

#[derive(PartialEq, Eq, Hash)]
struct BorrowedPair<'a, 'b, A: 'a, B: 'b>(&'a A, &'b B);

// See explanation (2).
trait KeyPair<A, B> {
    /// Obtains the first element of the pair.
    fn a(&self) -> &A;
    /// Obtains the second element of the pair.
    fn b(&self) -> &B;
}

// See explanation (3).
impl<'a, A, B> Borrow<KeyPair<A, B> + 'a> for Pair<A, B>
where
    A: Eq + Hash + 'a,
    B: Eq + Hash + 'a,
{
    fn borrow(&self) -> &(KeyPair<A, B> + 'a) {
        self
    }
}

// See explanation (4).
impl<'a, A: Hash, B: Hash> Hash for (KeyPair<A, B> + 'a) {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.a().hash(state);
        self.b().hash(state);
    }
}

impl<'a, A: Eq, B: Eq> PartialEq for (KeyPair<A, B> + 'a) {
    fn eq(&self, other: &Self) -> bool {
        self.a() == other.a() && self.b() == other.b()
    }
}

impl<'a, A: Eq, B: Eq> Eq for (KeyPair<A, B> + 'a) {}

// OP's Table struct
pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    map: HashMap<Pair<A, B>, f64>,
}

impl<A: Eq + Hash, B: Eq + Hash> Table<A, B> {
    fn new() -> Self {
        Table { map: HashMap::new() }
    }

    fn get(&self, a: &A, b: &B) -> f64 {
        *self.map.get(&BorrowedPair(a, b) as &KeyPair<A, B>).unwrap()
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        self.map.insert(Pair(a, b), v);
    }
}

// Boring stuff below.

impl<A, B> KeyPair<A, B> for Pair<A, B>
where
    A: Eq + Hash,
    B: Eq + Hash,
{
    fn a(&self) -> &A {
        &self.0
    }
    fn b(&self) -> &B {
        &self.1
    }
}
impl<'a, 'b, A, B> KeyPair<A, B> for BorrowedPair<'a, 'b, A, B>
where
    A: Eq + Hash + 'a,
    B: Eq + Hash + 'b,
{
    fn a(&self) -> &A {
        self.0
    }
    fn b(&self) -> &B {
        self.1
    }
}

//----------------------------------------------------------------

#[derive(Eq, PartialEq, Hash)]
struct A(&'static str);

#[derive(Eq, PartialEq, Hash)]
struct B(&'static str);

fn main() {
    let mut table = Table::new();
    table.set(A("abc"), B("def"), 4.0);
    table.set(A("123"), B("456"), 45.0);
    println!("{:?} == 45.0?", table.get(&A("123"), &B("456")));
    println!("{:?} == 4.0?", table.get(&A("abc"), &B("def")));
    // Should panic below.
    println!("{:?} == NaN?", table.get(&A("123"), &B("def")));
}

Explication:

  1. Nous avons introduit les types Pair et BorrowedPair. Nous ne pouvons pas utiliser (A, B) directement à cause de la règle Orphan E0210 . Cela convient car la carte est un détail de la mise en œuvre.

  2. Le trait KeyPair prend le rôle de Q mentionné ci-dessus. Nous aurions besoin de impl Eq + Hash for KeyPair, mais Eq et Hash ne sont pas tous les deux object safe . Nous ajoutons les méthodes a() et b() pour faciliter leur implémentation manuelle.

  3. Maintenant, nous implémentons le trait Borrow de Pair<A, B> à KeyPair + 'a. Notez le 'a - c’est un élément subtil qui est nécessaire pour que Table::get fonctionne réellement. Le 'a arbitraire nous permet de dire qu'un Pair<A, B> peut être emprunté à l'objet trait pour any cycle de vie. Si nous ne spécifions pas le 'a, l'objet trait non dimensionné sera par défaut à 'static , ce qui signifie que le trait Borrow ne peut être appliqué que si une implémentation telle que BorrowedPair survit à 'static, ce qui n'est certainement pas le cas. 

  4. Enfin, nous implémentons Eq et Hash. Comme ci-dessus, nous implémentons pour KeyPair + 'a au lieu de KeyPair (ce qui signifie KeyPair + 'static dans ce contexte).


L'utilisation d'objets trait générera des coûts indirectionnels lors du calcul du hachage et de la vérification de l'égalité dans get(). Le coût peut être éliminé si l'optimiseur est capable de le devirtualiser, mais on ignore si LLVM le fera.

Une alternative consiste à stocker la carte sous la forme HashMap<(Cow<A>, Cow<B>), f64>. L'utilisation de cette méthode nécessite moins de "code intelligent", mais il y a maintenant un coût en mémoire pour stocker l'indicateur de propriété/emprunté ainsi que le coût d'exécution à la fois dans get() et set().

À moins que vous n'utilisiez la variable HashMap standard et que vous ajoutiez une méthode pour rechercher une entrée uniquement via Hash + Eq, il n'existe aucune solution à coût zéro.

23
kennytm

Dans la méthode get, les valeurs empruntées par a et b peuvent ne pas être adjacentes en mémoire.

[--- A ---]      other random stuff in between      [--- B ---]
 \                                                 /
  &a points to here                               &b points to here

Emprunter une valeur de type &(A, B) nécessiterait une A et une B adjacentes.

     [--- A ---][--- B ---]
      \
       we could have a borrow of type &(A, B) pointing to here

Un peu de code non sécurisé peut résoudre ce problème! Nous avons besoin d’une copie superficielle de *a et *b.

use std::collections::HashMap;
use std::hash::Hash;
use std::mem::ManuallyDrop;
use std::ptr;

#[derive(Debug)]
pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    map: HashMap<(A, B), f64>
}

impl<A: Eq + Hash, B: Eq + Hash> Table<A, B> {
    fn get(&self, a: &A, b: &B) -> f64 {
        unsafe {
            // The values `a` and `b` may not be adjacent in memory. Perform a
            // shallow copy to make them adjacent. This should be fast! This is
            // not a deep copy, so for example if the type `A` is `String` then
            // only the pointer/length/capacity are copied, not any of the data.
            //
            // This makes a `(A, B)` backed by the same data as `a` and `b`.
            let k = (ptr::read(a), ptr::read(b));

            // Make sure not to drop our `(A, B)`, even if `get` panics. The
            // caller or whoever owns `a` and `b` will drop them.
            let k = ManuallyDrop::new(k);

            // Deref `k` to get `&(A, B)` and perform lookup.
            let v = self.map.get(&k);

            // Turn `Option<&f64>` into `f64`.
            *v.unwrap()
        }
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        self.map.insert((a, b), v);
    }
}

fn main() {
    let mut table = Table { map: HashMap::new() };
    table.set(true, true, 1.0);
    table.set(true, false, 2.0);

    println!("{:#?}", table);

    let v = table.get(&true, &true);
    assert_eq!(v, 1.0);
}
5
dtolnay

Un trait Memory qui prend deux clés, set par valeur et get par référence:

trait Memory<A: Eq + Hash, B: Eq + Hash> {

    fn get(&self, a: &A, b: &B) -> Option<&f64>;

    fn set(&mut self, a: A, b: B, v: f64);
}

Vous pouvez impl un tel trait en utilisant une carte de cartes:

pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    table: HashMap<A, HashMap<B, f64>>,
}   

impl<A: Eq + Hash, B: Eq + Hash> Memory<A, B> for Table<A, B> {

    fn get(&self, a: &A, b: &B) -> Option<&f64> {
        self.table.get(a)?.get(b)
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        let inner = self.table.entry(a).or_insert(HashMap::new());
        inner.insert(b, v);
    }
}

Veuillez noter que si la solution est assez élégante, l’empreinte mémoire d’un HashMap de HashMaps doit être prise en compte lorsque des milliers d’instances HashMap doivent être gérées.

Exemple complet

0
attdona