Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

Learn RustChapter 6: Collections

Lesson 6.6 · 8 min

Chapter review: Collections

Chatterbox remembers the whole conversation now: it can number it, count it, search it, keep facts by name and find your favourite words. Before chapter 7 teaches it to report errors, check that the five ideas behind that are solid: a mixed quiz, then one bigger exercise that needs all of them.

Adds to ChatterboxNothing new: a mixed quiz on the whole chapter, and a bigger exercise that uses all of it.

What chapter 6 gave Chatterbox

At the end of chapter 5, Chatterbox had good types but forgot every message as soon as it answered it. Now it keeps them: a numbered /history, /count, /remember and /recall, /find and /stats. Five ideas did that:

  • 6.1 Vec: a growable list that owns its items; [ ] crashes on a missing position, get gives an Option.
  • 6.2 walking a list: for m in &history borrows and keeps the list; enumerate adds the position, counting from 0.
  • 6.3 HashMap: rows of key and value, found by name; entry and or_insert count things in one line; no order.
  • 6.4 closures: small functions written on the spot that capture the names around them, by borrowing them.
  • 6.5 iterators: a value with a next(), wrapped in steps like filter and map, that does nothing until something at the end asks.

Nothing new comes in this lesson. Every answer below was produced by the real compiler, so a wrong guess points straight at the lesson to reopen.

Mixed quiz

The questions jump between the five ideas on purpose, because a real program uses them all at once.

PredictThe third message
fn main() {
let history = vec![String::from("hi"), String::from("hello")];
match history.get(2) {
Some(m) => println!("third: {m}"),
None => println!("only {} messages", history.len()),
}
}

What does this print?

  • only 2 messages
  • third: hello
  • it panics
Reveal the answer

The program prints: only 2 messages Positions count from 0, so a two-item list has positions 0 and 1, and 2 is past the end. get never crashes: it answers None, and the None arm runs. history[2] would have panicked instead. Lesson 6.1.

PredictA closure that counts
fn main() {
let mut history = vec![String::from("hi")];
let count = || history.len();
history.push(String::from("hello"));
println!("{}", count());
}

Does this compile?

  • It compiles and runs
  • Compile error E0502
  • Compile error E0382
Reveal the answer

It does not compile: E0502. The closure captures history by borrowing it to look, and that borrow lasts until its last call, on the final line. push needs an exclusive borrow in the middle of that stretch, so the compiler refuses. Call count() before the push, or make the closure after it. Lesson 6.4, with the rule from lesson 4.4.

error[E0502]: cannot borrow `history` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let count = || history.len();
  |                 -- ------- first borrow occurs due to use of `history` in closure
  |                 |
  |                 immutable borrow occurs here
4 |     history.push(String::from("hello"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", count());
  |                    ----- immutable borrow later used here
PredictHi, hi, HI
use std::collections::HashMap;
 
fn main() {
let mut seen: HashMap<String, u32> = HashMap::new();
for word in ["Hi", "hi", "HI", "bye"] {
*seen.entry(word.to_lowercase()).or_insert(0) += 1;
}
println!("{} {:?}", seen.len(), seen.get("hi"));
}

What does this print?

  • 2 Some(3)
  • 4 Some(1)
  • 2 Some(1)
Reveal the answer

The program prints: 2 Some(3) Every word is lowercased before it becomes a key, so the three greetings land in one row and bye in another: two rows. entry(…).or_insert(0) makes the row at 0 the first time and the += 1 adds to it every time, so that row reaches 3. Lesson 6.3.

PredictAdd a ! to every message
fn main() {
let mut history = vec![String::from("hi"), String::from("yo")];
for m in history.iter() {
m.push('!');
}
println!("{:?}", history);
}

Does this compile?

  • It compiles and runs
  • Compile error E0596
  • Compile error E0382
Reveal the answer

It does not compile: E0596. iter() lends each message to look at, and push wants to change it, which a looking borrow cannot do. The walk that lends each item to change is iter_mut() (or &mut history). Making history itself mut is not enough: the kind of walk decides. Lesson 6.5 has the table of all three walks.

error[E0596]: cannot borrow `*m` as mutable, as it is behind a `&` reference
 --> src/main.rs:4:9
  |
3 |     for m in history.iter() {
  |              -------------- this iterator yields `&` references
4 |         m.push('!');
  |         ^ `m` is a `&` reference, so it cannot be borrowed as mutable
  |
help: use mutable method
  |
3 |     for m in history.iter_mut() {
  |                          ++++
PredictThe first long one
fn main() {
let history = vec!["ok", "sure thing", "yes", "absolutely"];
for (i, m) in history.iter().enumerate() {
if m.len() > 3 {
println!("{i}");
break;
}
}
}

What does this print?

  • 1
  • 2
  • 3
Reveal the answer

The program prints: 1 enumerate numbers from 0, so "ok" is 0 and "sure thing", the first one longer than 3 bytes, is 1. break leaves the loop right there, so "absolutely" is never looked at. If you said 2, you counted from 1: lesson 6.2.

PredictShortest name first
fn main() {
let mut names = vec!["sam", "alexandra", "jo", "ada"];
names.sort_by_key(|n| n.len());
println!("{:?}", names);
}

What does this print?

  • ["jo", "sam", "ada", "alexandra"]
  • ["jo", "ada", "sam", "alexandra"]
  • ["alexandra", "sam", "ada", "jo"]
Reveal the answer

The program prints: ["jo", "sam", "ada", "alexandra"] The closure hands sort_by_key each name's length, and it sorts by that, shortest first. "sam" and "ada" both have 3 letters, and items with equal keys keep the order they had, so "sam" stays in front. It is not alphabetical: only the length was compared. Lesson 6.4.

PredictAdd up the lengths
fn main() {
let history = vec![String::from("hi"), String::from("hello")];
let total = history.iter().map(|m| m.len()).sum();
println!("{total}");
}

Does this compile?

  • It compiles and runs
  • Compile error E0283
  • Compile error E0382
Reveal the answer

It does not compile: E0283. sum(), like collect(), can build more than one type, and nothing here says which: total has no type. The compiler stops with "type annotations needed" and even suggests let total: usize. Lesson 6.5 met the same error on collect().

error[E0283]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let total = history.iter().map(|m| m.len()).sum();
  |         ^^^^^                                   --- type must be known at this point
  |
  = note: the type must implement `Sum<usize>`
help: the trait `Sum` is implemented for `usize`
 --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/accum.rs:50:8
 ::: /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/accum.rs:206:0
  |
  = note: in this macro invocation
note: required by a bound in `std::iter::Iterator::sum`
 --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/iterator.rs:3665:4
  = note: this error originates in the macro `integer_sum_product` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider giving `total` an explicit type, where the type for type parameter `S` is specified
  |
3 |     let total: usize = history.iter().map(|m| m.len()).sum();
  |              +++++++
PredictThe first two long ones, loudly
fn main() {
let history = vec!["hi", "hello", "hey", "howdy", "hiya there"];
let loud: Vec<String> = history
.iter()
.filter(|m| m.len() > 3)
.map(|m| m.to_uppercase())
.take(2)
.collect();
println!("{}", loud.join(" "));
}

What does this print?

  • HELLO HOWDY
  • HI HELLO
  • HELLO HOWDY HIYA THERE
Reveal the answer

The program prints: HELLO HOWDY Each item goes through every step in order: filter drops "hi" and "hey", map capitalises what is left, and take(2) stops after two, so "hiya there" is never even looked at. collect() knows to build a Vec<String> from the type on loud. Lesson 6.5.

Milestone: who talks most, and where

Time to use all five ideas together. Imagine Chatterbox in a group chat, with several people in the history. A /digest command would want two answers: who has said the most, and where a word came up. Write the three functions behind it. They take the history as &[Message], a borrowed view of the list, like every exercise in this chapter.

The doc comments (the /// lines) say exactly what each must return. One part of leaderboard needs care: a HashMap has no order, so its rows have to be put into a Vec and sorted, with a rule for ties, or the answer could change from run to run. Lesson 6.5 sorted /stats exactly this way.

Try itMilestone: tally, leaderboard and search

Fill in the three todo!() bodies. tally counts messages per author in a HashMap. leaderboard turns that count into lines like sam: 3, most first, ties in alphabetical order. search gives the /history numbers of the messages that contain a word, ignoring capitals. The hidden tests use a six-message chat, ties, an empty history and a word nobody said.

Fill in the three todo!() bodies. tally counts messages per author in a HashMap. leaderboard turns that count into lines like sam: 3, most first, ties in alphabetical order. search gives the /history numbers of the messages that contain a word, ignoring capitals. The hidden tests use a six-message chat, ties, an empty history and a word nobody said.

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() }
}
}
 
/// How many messages each author wrote: one row per author.
fn tally(history: &[Message]) -> HashMap<String, u32> {
todo!()
}
 
/// One line per author, like "sam: 2", most messages first.
/// Authors with the same count are in alphabetical order.
fn leaderboard(history: &[Message]) -> Vec<String> {
todo!()
}
 
/// The /history numbers (counting from 1) of every message whose
/// text contains word, ignoring capitals on both sides.
fn search(history: &[Message], word: &str) -> Vec<usize> {
todo!()
}
 
fn main() {
let history = vec![
Message::new("sam", "Rust is fun"),
Message::new("ada", "is it fast?"),
Message::new("sam", "very, and rust is safe too"),
];
println!("{:?}", leaderboard(&history));
println!("{:?}", search(&history, "RUST"));
}

Hidden tests (8): an_empty_history_has_no_rows leaderboard_puts_the_most_first leaderboard_ties_are_alphabetical search_ignores_capitals_on_both_sides search_with_no_hits_is_empty search_numbers_from_one tally_counts_each_author the_history_is_only_borrowed

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

Where to go from here

If a quiz answer surprised you, reopen the lesson its explanation names before going on. Chapter 7 keeps all of this and asks a new question: what should a function return when it can fail? So far a missing item was an Option and nothing more. Next, a failure gets to say why.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
When do you reach for a Vec, and when for a HashMap?Reveal
A Vec when you ask by position or care about order ("the third message"). A HashMap when you ask by name ("what goes with pizza"), and do not need any order.
What do <code>iter()</code>, <code>iter_mut()</code> and <code>into_iter()</code> each hand you?Reveal
A borrow to look at each item, an exclusive borrow to change each item, and each item itself, which uses the collection up.
How does a closure use a name from around it, and how long does that last?Reveal
It captures it by borrowing it (exclusively if it changes it). The borrow lasts until the closure's last use, and the usual borrowing rules apply meanwhile.
Why does <code>let total = v.iter().map(…).sum();</code> fail, and how do you fix it?Reveal
sum() can produce more than one type and nothing says which. Give the name a type, as in let total: usize.
Your Chatterbox so far: v0.6 (after lesson 6.5)
Chatterbox v0.6 after lesson 6.5: the whole program
src/main.rs
// Chatterbox: a small chat program, built one lesson at a time.
use std::collections::HashMap;
use std::io;
 
/// What the bot is called. A constant: fixed for the whole program.
const BOT: &str = "Chatterbox";
/// The version as a pair of numbers: (major, minor).
const VERSION: (u32, u32) = (0, 6);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
/// What /help prints.
const HELP: &str = "commands: /help, /again, /history, /count, /stats, /remember <name> <fact>, /recall <name>, /find <word>, /say <text>, /shout <text>, /debug <text>, /quit (or /h, /q, /exit)";
 
/// One line of the conversation: who said it, and what they said.
struct Message {
author: String,
text: String,
}
 
impl Message {
/// Make a Message from two pieces of borrowed text.
fn new(author: &str, text: &str) -> Message {
Message {
author: String::from(author),
text: String::from(text),
}
}
 
/// The message as one transcript line: "you: hello".
fn to_line(&self) -> String {
format!("{}: {}", self.author, self.text)
}
 
/// The text in capitals, for /shout.
fn shout(&self) -> String {
self.text.to_uppercase()
}
}
 
/// Everything a line of input can mean. One enum, so a `match` on it must
/// handle every kind of line: the compiler checks that nothing is forgotten.
#[derive(Debug)]
enum Command {
Empty,
Help,
Quit,
Again,
History,
Count,
Stats,
Remember(String, String),
Recall(Option<String>),
Usage(String),
Find(Option<String>),
Say(Option<String>),
Shout(Option<String>),
Unknown(String),
Debug(Option<String>),
Chat(String),
}
 
/// A borrowed Option<&str> turned into an owned Option<String>, so the
/// Command can keep the text after `line` is gone.
fn owned(text: Option<&str>) -> Option<String> {
match text {
Some(t) => Some(String::from(t)),
None => None,
}
}
 
/// Turn one line of input into a Command.
fn parse(line: &str) -> Command {
match first_word(line) {
"" => Command::Empty,
"/help" | "/h" => Command::Help,
"/quit" | "/q" | "/exit" => Command::Quit,
"/again" => Command::Again,
"/history" => Command::History,
"/count" => Command::Count,
"/stats" => Command::Stats,
"/remember" => match arg(line) {
Some(rest) => match arg(rest) {
Some(fact) => Command::Remember(String::from(first_word(rest)), String::from(fact)),
None => Command::Usage(String::from("/remember <name> <fact>")),
},
None => Command::Usage(String::from("/remember <name> <fact>")),
},
"/recall" => Command::Recall(owned(arg(line))),
"/find" => Command::Find(owned(arg(line))),
"/debug" => Command::Debug(owned(arg(line))),
"/say" => Command::Say(owned(arg(line))),
"/shout" => Command::Shout(owned(arg(line))),
word => {
if is_command(word) {
Command::Unknown(String::from(word))
} else {
Command::Chat(String::from(line))
}
}
}
}
 
/// The first line Chatterbox prints.
fn banner() -> String {
format!("{} v{}.{}", BOT, VERSION.0, VERSION.1)
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str) -> String {
let mut text = format!("Hi {}! I'm {}.", name, BOT);
text.push_str(" Ask me anything.");
text
}
 
/// True when the text is a command: it starts with a slash.
fn is_command(text: &str) -> bool {
text.starts_with('/')
}
 
/// The first word of the text: a slice (&str) pointing into it, not a copy.
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(i) => &text[..i],
None => text,
}
}
 
/// The text after the command word, if there is any: Some("hello") for
/// "/say hello", None for a bare "/say". No more "" standing in for nothing.
fn arg(line: &str) -> Option<&str> {
match line.find(' ') {
Some(i) => {
let rest = line[i + 1..].trim();
if rest.is_empty() { None } else { Some(rest) }
}
None => None,
}
}
 
/// What the bot says back to an ordinary message (not a command).
fn reply(message: &str) -> String {
if message.len() > MAX_LEN {
format!("that is too long (the limit is {} bytes)", MAX_LEN)
} else if message.ends_with('?') {
String::from("Good question! I can only echo for now. Ask me again in chapter 9.")
} else {
format!("you said: {}", message)
}
}
 
fn main() {
let mut messages_seen: u32 = 0;
let quiet = false;
let mut last: Option<String> = None; // the last thing you said, for /again
let mut history: Vec<Message> = Vec::new(); // everything said so far, in order
let mut facts: HashMap<String, String> = HashMap::new(); // name -> fact, for /recall
 
println!("{}", banner());
println!("What should I call you?");
let mut name_line = String::new();
io::stdin().read_line(&mut name_line).unwrap();
let name = name_line.trim();
if !quiet {
println!("{}", greeting(name));
}
 
let mut line = String::new();
loop {
line.clear();
let bytes = io::stdin().read_line(&mut line).unwrap();
if bytes == 0 {
break; // no more input: the keyboard was closed
}
let message = line.trim();
match parse(message) {
Command::Empty => {} // nothing typed: nothing to answer
Command::Quit => {
println!("bot: bye, {}!", name);
break;
}
Command::Help => println!("bot: {}", HELP),
Command::Again => {
if let Some(text) = &last {
println!("bot: {}", reply(text));
} else {
println!("bot: nothing to repeat yet");
}
}
Command::History => {
if history.is_empty() {
println!("bot: nothing said yet");
} else {
for (i, m) in history.iter().enumerate() {
println!("{}. {}", i + 1, m.to_line());
}
}
}
Command::Count => {
let bytes: usize = history.iter().map(|m| m.text.len()).sum();
println!("bot: {} messages, {} bytes of text", history.len(), bytes);
}
Command::Stats => {
// One chain of steps: your messages -> their words -> a count per word.
let mut counts: HashMap<String, u32> = HashMap::new();
for word in history
.iter()
.filter(|m| m.author == "you")
.flat_map(|m| m.text.split_whitespace())
{
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
let mut top: Vec<(String, u32)> = counts.into_iter().collect();
// most used first; ties in alphabetical order, so the answer never changes
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let words: Vec<String> = top
.iter()
.take(3)
.map(|(word, n)| format!("{} ({})", word, n))
.collect();
if words.is_empty() {
println!("bot: nothing said yet");
} else {
println!("bot: your top words: {}", words.join(", "));
}
}
Command::Remember(key, fact) => {
println!("bot: got it: {} is {}", key, fact);
facts.insert(key, fact);
}
Command::Recall(Some(key)) => match facts.get(&key) {
Some(fact) => println!("bot: {} is {}", key, fact),
None => println!("bot: I don't know anything about {}", key),
},
Command::Recall(None) => {
if facts.is_empty() {
println!("bot: I don't remember anything yet. Try /remember");
} else {
// A HashMap has no order, so sort the names before printing them.
let mut names = Vec::new();
for name in facts.keys() {
names.push(name.as_str());
}
names.sort();
println!("bot: I remember: {}", names.join(", "));
}
}
Command::Find(Some(word)) => {
// The closure borrows `word` from here: a small function made on the spot.
let hits: Vec<&Message> = history
.iter()
.filter(|m| m.author == "you" && m.text.contains(&word))
.collect();
if hits.is_empty() {
println!("bot: you never said {}", word);
} else {
for m in hits {
println!("{}", m.to_line());
}
}
}
Command::Find(None) => println!("bot: find what? Try /find hello"),
Command::Usage(form) => println!("bot: try {}", form),
Command::Say(Some(text)) => println!("bot: {}", text),
Command::Say(None) => println!("bot: say what? Try /say hello"),
Command::Shout(Some(text)) => println!("bot: {}", Message::new("you", &text).shout()),
Command::Shout(None) => println!("bot: shout what? Try /shout hello"),
Command::Unknown(word) => {
println!("bot: I don't know the command {}. Try /help.", word)
}
Command::Debug(Some(text)) => println!("bot: {:?}", parse(&text)),
Command::Debug(None) => println!("bot: debug what? Try /debug /say hi"),
Command::Chat(text) => {
messages_seen += 1;
let said = Message::new("you", &text);
let answer = Message::new("bot", &reply(&said.text));
println!("bot: {}", answer.text);
history.push(said);
history.push(answer);
// Message::new made the history its own copy, so `text` itself can
// be handed to `last` for /again: no extra copy needed.
last = Some(text);
}
}
}
println!("messages so far: {}", messages_seen);
}

Download this version (zip)Unzip, then cargo run inside the folder.

That is lesson 6.6. Tick it off so the course remembers where you are. That is also the end of chapter 6: a few minutes with the course flashcards now makes it stick, and the chapter’s cheat sheet has every idea on one screen.

Questions people ask

What should I know before learning Rust error handling?

How to keep values in a Vec and a HashMap, how to walk them with a for loop or an iterator chain without giving them away, and how Option works, because Result in chapter 7 is built the same way as Option.

What is the difference between iter and into_iter in Rust?

iter lends each item to look at, so the collection is still there afterwards. into_iter hands over each item itself, so the collection is used up by the walk.

Why does a HashMap print its keys in a different order each time?

A HashMap keeps no order, and the order it walks its rows in can change from run to run. If you need a stable order, copy the rows into a Vec and sort it.

Do Rust iterator chains run when you write them?

No. Steps like map and filter only build a bigger iterator. The work happens when something at the end, such as collect, sum, count or a for loop, asks for the items, and then each item goes through every step in turn.

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