Learning Rust šŸ¦€: 18 - Rust Collections: HashMaps, accessing values with keys instead of indices

fadygrab

Fady GA šŸ˜Ž

Posted on October 20, 2023

Learning Rust šŸ¦€: 18 - Rust Collections: HashMaps, accessing values with keys instead of indices

I'll continue Rust's collections discusion that started two articles ago with Vectors and Strings as a special kind of Vectors. In this last dedicated article about collections, I'll bring out the HashMaps! Let's begin.

āš ļø Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.

āš ļøāš ļø The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

ā­ I try to publish a new article every week (maybe more if the Rust gods šŸ™Œ are generous šŸ˜) so stay tuned šŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and X (fromarly Twitter).

Table of Contents:

What is a Hash Map:

Hash Maps aren't something native to Rust. Other programming languages has similar structures but with different names, like Dictionaries in Python, Objects in Javascrip, Hash Tables in Powershell (althoug not a programming language per-sey), and many more.
They work more or less the same way as you would expect! You retreive a Value with a Key instead of and index as in Vectors.
Rust's HashMap is pretty similar to Python's Dictionary except that in Rust, the HashMap must have the keys of the same type and the values also must be of the same type but the keys and values can be of similar of different types than eachother.

Hash Maps is written in Rust as HashMap<K, V> where K is the keys' type and V is the values' type which can be the same.

I'll demonstrate here the basic API of Rust's Hash Maps but as always, the documentations contains a lot of other metods that can be very useful.

Creating a Hash Map:

Appearently Hash Maps isn't used much in Rust that they aren't loaded in the prelude like Vectors and Strings. So, we have to "import" them first wiht the use keyword then use the new method to create a new one. In the Example below, I'me creating a map HashMap with the keys of type String and the values of type u8. It should represent the scores that students have obtained after taking and exam. I then inserted the scores of two students, Bob and Ben:

use std::collections::HashMap;
fn main() {
    // Create a Hash Map
    let mut map: HashMap<String, u8> = HashMap::new();

    map.insert(String::from("Bob"), 30);
    map.insert(String::from("Ben"), 25);
    println!("{map:?}");
}
Enter fullscreen mode Exit fullscreen mode

Here, I've used the insert method to insert the student names (keys) and there exam scores (values). The println output will be:

{"Bob": 30, "Ben": 25}
Enter fullscreen mode Exit fullscreen mode

Accessing values in a Hash Map:

To access a certain value in a Hash Map, we can use the get method wich takes the key as its parameter. The get method retun an Option<&V> which will return Some<&V> in case if the key exists or None if it doesn't. For Example, to get Bob's score we can use the follwing code:

let bob_score = map.get("Bob").copied().unwrap_or(0);
println!("Bob scored {bob_score}");
Enter fullscreen mode Exit fullscreen mode

OK, this looks a little strange. Let's break it down.
As I've mentioned, the get method returns the Option<&V> enum of a reference for the values type (here it is &u8). The copied method will return Option<V> instead of Option<&V> i.e. Option<u8> as I don't want to return a reference of the value. Then the unwrap_or method will return V if the Option enum is Some or a default value that it takes as a parameter. So, bob_score will hold eventually the value of the "Bob" key with u8 type and it will be printed as follows:

Bob scored 30
Enter fullscreen mode Exit fullscreen mode

If we haven't used copied here, we would write something strange as the paramtere of unwrap_or like unwrap_or(&0) and bob_scrore will be a refrence of the retrieved value.

Another way to access values inside a Hash Map is to use a for loop as follows:

for (key, value) in &map {
    println!("{}: {}", key, value);
}
Enter fullscreen mode Exit fullscreen mode

This will print the following as expected:

Ben: 25
Bob: 30
Enter fullscreen mode Exit fullscreen mode

One more thing before we can leave this section is that Mash Maps takes ownership over the passed keys and values. Look at the follwing example:

let jane = String::from("Jane");
let jane_score = 29;
map.insert(jane, jane_score);

println!("{jane} scored {jane_score}"); // Error: borrow of moved value: `jane`
Enter fullscreen mode Exit fullscreen mode

Here we have declared jane as String and her score jane_score as i32. As we have used the insert method, jane has "moved" into the Hash Map as it's a String while jane_score is "copied" as it's a simple type. The println will cause a compilation error as jane isn't valid anymore after the usage of insert.

Updating a Hash Map:

We usualy update a Hash Map in one of the following scenarios:

  • Overwite the old values
  • Only update if the key isn't present
  • Update a value based on its old value Let's see how we can perform each.

Overwrite the values:

This is probably the simplest implementation. We just have to use insert using a pre-existing key.

map.insert(String::from("Jane", 29);
let jane_score = map.get("Jane").copied().unwrap();
println!("Jane score before the update {jane_score}");

map.insert(String::from("Jane"), 30);
let jane_score = map.get("Jane").copied().unwrap();
println!("Jane score after the update {jane_score}");
Enter fullscreen mode Exit fullscreen mode

Here, we have updated Jane's score from 29 to 30

Jane score before the update 29
Jane score after the update 30
Enter fullscreen mode Exit fullscreen mode

Only update if the key isn't present:

In this use case, we use the entry method that takes a key as a parameter and returns Entry enum representing an entry that could exist or not. Then we use a similarly useful method, or_insert that insert the passed value if the entry doesn't exist. Let's look at this example:

map.entry(String::from("Jane")).or_insert(25);
map.entry(String::from("Windy")).or_insert(25);
println!("{map:?}");
Enter fullscreen mode Exit fullscreen mode

Here, we were retrieving the Jane and Windy entries and inserting 25 as their exam score if they arent present. As Jane entry already exist from the last section, it will remain unchanged with the value of 30. As for Windy, because it's a new key, it will be inserted with the value of 25 and the println will print the following:

{"Ben": 25, "Windy": 25, "Bob": 30, "Jane": 30}
Enter fullscreen mode Exit fullscreen mode

Update a value based on its old value:

Let's say that we will give all students a 5 points bonus. So, we will have a new value of a certain key that is the result of the addition of its old value and the 5 points bonus. One way to do this is as follows:

for (_, score) in map.iter_mut() {
    *score += 5;
}
println!("{map:?}");
Enter fullscreen mode Exit fullscreen mode

Let's explain what's going on here.
First, I've used the iter_mut method that returns "mutable" references of the keys and values of the Hash Map because I want to change the values. And as I don't care about the keys, I've used the underscore in its place in the for expression. Now, score is a &mut u8 or a mutable reference of the values of u8 type. Next, I've used the "de-referencing" operator "*" on score. We will have a look at de-referencing later, but let's just say it's a way to get the value that the reference is "referencing".

If I didn't use the "*" here, the compilier will complain about not being able to add {integer} refering to the 5 and &mut u8 (the score) as it considers them of different types.

Then I've added the 5 points bonus to the old values.
The result of this will be all the values have increase by 5 from their old values (compare them to the last println output):

{"Ben": 30, "Windy": 30, "Bob": 35, "Jane": 35}
Enter fullscreen mode Exit fullscreen mode

We have now looked at Hash Maps and saw how they are almost similar to Python's Dictionaries except for the types restrictions.
Next we will look at Rust's Error Handling. See you then šŸ‘‹

šŸ’– šŸ’Ŗ šŸ™… šŸš©
fadygrab
Fady GA šŸ˜Ž

Posted on October 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related