HashMap

Our implementation of Index/IndexMut is not ideal: we need to iterate over the entire Vec to retrieve a ticket by id; the algorithmic complexity is O(n), where n is the number of tickets in the store.

We can do better by using a different data structure for storing tickets: a HashMap<K, V>.

#![allow(unused)]
fn main() {
use std::collections::HashMap;

// Type inference lets us omit an explicit type signature (which
// would be `HashMap<String, String>` in this example).
let mut book_reviews = HashMap::new();

book_reviews.insert(
    "Adventures of Huckleberry Finn".to_string(),
    "My favorite book.".to_string(),
);
}

HashMap works with key-value pairs. It's generic over both: K is the generic parameter for the key type, while V is the one for the value type.

The expected cost of insertions, retrievals and removals is constant, O(1). That sounds perfect for our usecase, doesn't it?

Key requirements

There are no trait bounds on HashMap's struct definition, but you'll find some on its methods. Let's look at insert, for example:

#![allow(unused)]
fn main() {
// Slightly simplified
impl<K, V> HashMap<K, V>
where
    K: Eq + Hash,
{
    pub fn insert(&mut self, k: K, v: V) -> Option<V> {
        // [...]
    }
}
}

The key type must implement the Eq and Hash traits.
Let's dig into those two.

Hash

A hashing function (or hasher) maps a potentially infinite set of a values (e.g. all possible strings) to a bounded range (e.g. a u64 value).
There are many different hashing functions around, each with different properties (speed, collision risk, reversibility, etc.).

A HashMap, as the name suggests, uses a hashing function behind the scene. It hashes your key and then uses that hash to store/retrieve the associated value. This strategy requires the key type must be hashable, hence the Hash trait bound on K.

You can find the Hash trait in the std::hash module:

#![allow(unused)]
fn main() {
pub trait Hash {
    // Required method
    fn hash<H>(&self, state: &mut H)
       where H: Hasher;
}
}

You will rarely implement Hash manually. Most of the times you'll derive it:

#![allow(unused)]
fn main() {
#[derive(Hash)]
struct Person {
    id: u32,
    name: String,
}
}

Eq

HashMap must be able to compare keys for equality. This is particularly important when dealing with hash collisions—i.e. when two different keys hash to the same value.

You may wonder: isn't that what the PartialEq trait is for? Almost!
PartialEq is not enough for HashMap because it doesn't guarantee reflexivity, i.e. a == a is always true.
For example, floating point numbers (f32 and f64) implement PartialEq, but they don't satisfy the reflexivity property: f32::NAN == f32::NAN is false.
Reflexivity is crucial for HashMap to work correctly: without it, you wouldn't be able to retrieve a value from the map using the same key you used to insert it.

The Eq trait extends PartialEq with the reflexivity property:

#![allow(unused)]
fn main() {
pub trait Eq: PartialEq {
    // No additional methods
}
}

It's a marker trait: it doesn't add any new methods, it's just a way for you to say to the compiler that the equality logic implemented in PartialEq is reflexive.

You can derive Eq automatically when you derive PartialEq:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Eq)]
struct Person {
    id: u32,
    name: String,
}
}

Eq and Hash are linked

There is an implicit contract between Eq and Hash: if two keys are equal, their hashes must be equal too. This is crucial for HashMap to work correctly. If you break this contract, you'll get nonsensical results when using HashMap.

Exercise

The exercise for this section is located in 06_ticket_management/15_hashmap