Vibe Engines
YouTube

Lesson 6.3 · 9 min

Lookup by key: HashMap

A Vec finds things by position. But "how many messages has ada sent?" is a question about a name, not a position. For lookups by name, Rust has the HashMap, and it follows the same owner rule as everything else.

Adds to ChatterboxA HashMap counts messages per author.

A table of keys and values

A HashMap is a table. Every row has a key (the thing you look up by) and a value (what you find). You write the type as HashMap<String, u32>: keys are Strings, values are numbers. It lives in the standard library, so a program that wants one starts with use std::collections::HashMap;.

The owner rule again: the map owns its keys and its values. When you insert a String key, it moves into the table.

insert puts a row in, get looks one up
use std::collections::HashMap;
 
fn main() {
let mut counts: HashMap<String, u32> = HashMap::new();
counts.insert("sam".to_string(), 3);
counts.insert("ada".to_string(), 1);
println!("{:?}", counts.get("sam"));
println!("{:?}", counts.get("zoe"));
}
$ cargo run
Some(3)
None

get returns an Option, as it did on a Vec: Some(3) when the key is there, None when it is not. A name nobody has used is not an error, just an empty answer.

What if you insert the same key twice?

PredictSame key, twice
use std::collections::HashMap;
 
fn main() {
let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert("sam".to_string(), 1);
scores.insert("sam".to_string(), 2);
println!("{:?}", scores.get("sam"));
}

What does this program print?

  • Some(1)
  • Some(2)
  • None
Reveal the answer

The program prints: Some(2) A key appears once in a map. A second insert with the same key replaces the old value, so the table holds one row for "sam" and its value is 2.

Counting: the entry API

Counting messages per user needs three steps: find the user's row, make one starting at 0 if it does not exist, then add 1. Rust does all three with one line.

counts.entry(name) points at the row for that key, whether it exists yet or not. .or_insert(0) says "if there is no row, create it with 0", and hands back a borrow of the value that lives in the table. The * in front means "the number the borrow points to", so += 1 changes the number inside the map.

One line counts every name
use std::collections::HashMap;
 
fn main() {
let mut counts: HashMap<String, u32> = HashMap::new();
for name in ["sam", "ada", "sam", "sam"] {
*counts.entry(name.to_string()).or_insert(0) += 1;
}
println!("sam said {} things", counts["sam"]);
println!("ada said {} things", counts["ada"]);
}
$ cargo run
sam said 3 things
ada said 1 things

No order

A HashMap does not remember the order you inserted rows, and the order it walks them in can change between runs. That is the price of fast lookup. If you want to print a map in a stable order, collect the keys into a Vec and sort it, which is what /stats does below.

Chatterbox v0.6: who talks most

Each person now has a name next to their line, so the loop walks pairs of (user, line). Every Say adds one to that user's count. A new /stats command prints each name and its count in alphabetical order. The history from the last lesson is untouched.

src/main.rs: Chatterbox v0.6
use std::collections::HashMap;
 
struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: author.to_string(), text: text.to_string() }
}
 
fn shout(&self) -> String {
self.text.to_uppercase()
}
 
fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
 
enum Command {
Help,
Quit,
History,
Stats,
Say(String),
Unknown(String),
}
 
fn parse(line: &str) -> Command {
match line {
"/help" => Command::Help,
"/quit" => Command::Quit,
"/history" => Command::History,
"/stats" => Command::Stats,
_ => match line.strip_prefix("/say ") {
Some(rest) => Command::Say(rest.to_string()),
None => Command::Unknown(line.to_string()),
},
}
}
 
fn main() {
let mut history: Vec<Message> = Vec::new();
let mut counts: HashMap<String, u32> = HashMap::new();
let lines = [
("sam", "/say hello there"),
("ada", "/say hi sam"),
("sam", "/say how is the new model?"),
("ada", "/stats"),
("sam", "/quit"),
];
for (user, line) in lines {
match parse(line) {
Command::Help => println!("commands: /help /history /stats /say /quit"),
Command::Quit => break,
Command::History => {
for (i, m) in history.iter().enumerate() {
println!("{}. {}: {}", i + 1, m.author, m.text);
}
}
Command::Stats => {
let mut names: Vec<&String> = counts.keys().collect();
names.sort();
for name in names {
println!("{name}: {}", counts[name]);
}
}
Command::Say(text) => {
history.push(Message::new(user, &text));
*counts.entry(user.to_string()).or_insert(0) += 1;
}
Command::Unknown(other) => println!("unknown command: {other}"),
}
}
}
$ cargo run
ada: 1
sam: 2
Try itYour turn: messages per author

Write count_by_author. Walk the history and return a map from each author name to how many messages that author wrote.

Write count_by_author. Walk the history and return a map from each author name to how many messages that author wrote.

use std::collections::HashMap;
 
struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: author.to_string(), text: text.to_string() }
}
}
 
fn count_by_author(history: &Vec<Message>) -> HashMap<String, u32> {
// one row per author, value = how many messages
HashMap::new()
}
 
fn main() {
let history = vec![Message::new("sam", "a"), Message::new("ada", "b"), Message::new("sam", "c")];
let counts = count_by_author(&history);
println!("sam: {:?}", counts.get("sam"));
}

Hidden tests (3): empty_history_has_no_rows counts_each_author unknown_author_is_absent

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

What you have now

Chatterbox answers two kinds of question: "what was said, in what order" (a Vec) and "how many has this person said" (a HashMap). Pick the container by the question you ask: by position, or by name. One more tool for this chapter: the tiny in-place functions you have been reading as |m| ….

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does counts.get("zoe") return when zoe was never inserted?Reveal
None. A missing key is an ordinary empty answer, not a crash.
What does *counts.entry(name).or_insert(0) += 1 do?Reveal
It finds the row for name, creates it with 0 if it does not exist, and adds one to the number stored in the map.
Why does /stats sort the names before printing?Reveal
A HashMap has no stable order, so to print in a predictable order you collect the keys into a Vec and sort it.

That is lesson 6.3. Tick it off so the course remembers where you are.

Questions people ask

What do I need to write to use HashMap in Rust?

Add use std::collections::HashMap; at the top of the file. It is in the standard library, not in the prelude.

What happens if I insert the same key twice?

The second value replaces the first. A map holds at most one value per key.

Is a Rust HashMap like a Python dict?

Yes for lookup by key. Differences: all keys share one type, all values share one type, and iteration order is not something you should rely on.

Learn Rust by building a chat app · Lesson 6.3 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026