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.5 · 12 min

One item at a time

Chatterbox wants a /stats command: your three most-used words. With plain loops that is a loop to pick your messages, a loop to split them into words, a loop to count, and a list to sort. An iterator lets you write it as one chain of steps that reads in the order you think it.

Adds to Chatterbox/stats: your most-used words, in one chain of steps.

Something that hands out the next item

You have been writing history.iter() since lesson 6.2 without asking what it gives you. It does not give you the items. It gives you an iterator: a small value whose one job is to hand out the next item when asked, and to say when there are none left. You ask by calling next(), and the answer is an Option from lesson 5.4: Some(item), or None once the list is used up.

Asking an iterator for the next item, by hand
fn main() {
let words = vec!["rust", "is", "fun"];
let mut it = words.iter();
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
}
$ cargo run
Some("rust")
Some("is")
Some("fun")
None

Three Somes, then None. The iterator has to be mut because asking changes it: it remembers how far it has got. That is all a for loop is. for w in words.iter() means "call next(), run the body with what is inside the Some, and stop at the first None", which is the while let loop of lesson 5.5 with a nicer name.

Going deeper How big is an iterator?

An iterator over a Vec does not copy the list. history.iter() holds two addresses into the list it borrows: where it is now, and where the list ends. next() moves the first one along by one item, and answers None when it reaches the second. On a 64-bit computer, where an address takes 8 bytes:

  • Vec<String>, the list itself: 24 bytes (where its Strings start in the heap, how many it has room for, how many are in use), however many Strings it holds.
  • history.iter(): 16 bytes, just the two addresses. It hands out borrows, never the Strings themselves.
  • history.into_iter(): 32 bytes. It has taken over the Vec's heap block, so it also keeps where that block starts and how big it is, in order to free it when it is done. It hands each String out as an owned value.

A walker is a handful of bytes, however long the list is.

Type InspectorA list and its two walkers

sizes measured with size_of on this compiler (64-bit), not quoted

TypeBytesLayout
Vec<String>24pointer (8 B) · capacity (8 B) · length (8 B)
std::slice::Iter<String>16where it is now (8 B) · where the list ends (8 B)
std::vec::IntoIter<String>32the heap block (8 B) · its capacity (8 B) · where it is now (8 B) · where it ends (8 B)

A look, a change, or the item itself

Chapter 4 said there are three ways to hand a value over: lend it to look at, lend it to change, or give it away. A Vec has one iterator for each, and that choice is the most important thing about a walk.

You writeEach step hands youAfterwards the Vec is
history.iter() (or &history)a borrow, &Message: look onlyuntouched
history.iter_mut() (or &mut history)an exclusive borrow, &mut Message: you may change itchanged, still yours
history.into_iter() (or plain history)the Message itself: you own it nowgone, used up by the walk

You have used the first row since lesson 6.2. The middle row looks like this. Each w is an exclusive borrow of one String inside the list, so push changes that String where it lives, and the Vec, still yours, comes out with every word changed:

iter_mut: change every item in place
fn main() {
let mut words = vec![String::from("hi"), String::from("hello")];
for w in words.iter_mut() {
w.push('!');
}
println!("{:?}", words);
}
$ cargo run
["hi!", "hello!"]
PredictWalk it away, then count it
fn main() {
let names = vec![String::from("sam"), String::from("ada")];
let mut kept = Vec::new();
for name in names.into_iter() {
kept.push(name);
}
println!("{} {}", kept.len(), names.len());
}

Does this compile — and if it does, does it run?

  • It compiles and runs
  • Compile error E0382
  • It compiles, then panics
Reveal the answer

It does not compile: E0382. into_iter() takes the Vec, so each name is an owned String that can be pushed into kept without a copy. The price is that names is used up, and asking it for its length afterwards is the "borrow of moved value" from lesson 4.3. With iter() you would get borrows instead, and kept would have to hold &Strings or clones.

error[E0382]: borrow of moved value: `names`
 --> src/main.rs:7:35
  |
2 |     let names = vec![String::from("sam"), String::from("ada")];
  |         ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait
3 |     let mut kept = Vec::new();
4 |     for name in names.into_iter() {
  |                       ----------- `names` moved due to this method call
...
7 |     println!("{} {}", kept.len(), names.len());
  |                                   ^^^^^ value borrowed here after move
  |
note: `std::iter::IntoIterator::into_iter` takes ownership of the receiver `self`, which moves `names`
 --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/collect.rs:312:17
help: you can `clone` the value and consume it, but this might not be your desired behavior
  |
4 |     for name in names.clone().into_iter() {
  |                      ++++++++

Steps that wrap steps

Since an iterator is just "something with a next()", you can wrap one in another. These wrappers are called adapters, and each one takes a closure (lesson 6.4) that says what to do with every item:

  • map(|m| …) turns each item into something else: a message into its length.
  • filter(|m| …) keeps only the items for which the closure says true.
  • flat_map(|m| …) turns each item into several and hands them all out in a row: a message into its words.
  • take(3) hands out the first three, then says None.

An adapter does no work when you write it. It only builds a bigger iterator around the one before. Something at the end of the chain has to keep calling next(): a consumer. collect() gathers everything into a collection, sum() adds it up, count() counts it, and a for loop is a consumer too. So who runs first?

PredictWhen does the work happen?
struct Message {
author: String,
text: String,
}
 
fn main() {
let history = vec![
Message { author: "sam".to_string(), text: "hi".to_string() },
Message { author: "ada".to_string(), text: "hello".to_string() },
];
let lengths = history.iter().map(|m| {
println!("looking at {}", m.author);
m.text.len()
});
println!("chain built");
let total: usize = lengths.sum();
println!("total {total}");
}

What is the FIRST line this program prints?

  • looking at sam
  • chain built
  • total 7
Reveal the answer

The program prints: chain built Lines 11 to 14 only build the chain: a map wrapped around an iterator, holding the closure, not running it. "chain built" prints first. Then sum() calls next() on the map, which calls next() on the inner iterator, gets sam's message, runs the closure on it, and passes the length back. Then ada. Then the total, 7.

Rust calls this lazy: work happens when an answer is asked for, one item at a time, all the way down the chain. Each item travels through every step before the next item starts, so there is no list of lengths stored halfway, and a take(3) at the end really does stop the work after three.

Telling collect what to build

collect() can build many kinds of collection: a Vec, a String, a HashMap. The chain alone does not say which, so you have to.

Compiler CoachThe compiler, translated

This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.

fn main() {
let said = "rust is fun";
let words = said.split_whitespace().collect();
println!("{:?}", words);
}
error[E0283]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let words = said.split_whitespace().collect();
  |         ^^^^^                           ------- type must be known at this point
  |
  = note: the type must implement `FromIterator<&str>`
note: required by a bound in `collect`
 --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/iterator.rs:2073:4
help: consider giving `words` an explicit type
  |
3 |     let words: Vec<_> = said.split_whitespace().collect();
  |              ++++++++

E0283 in the compiler error atlas →

  1. type annotations needed: the compiler cannot work out what words should be. collect() could make a Vec, a String or several other things from the same words.
  2. type must be known at this point, under collect: this is the call that has to know, because it is the one doing the building.
  3. the type must implement `FromIterator<&str>`: whatever you choose must be able to be built from a series of &str items. (That is a trait, a named ability; traits get their own lesson in chapter 8.)
  4. help: consider giving `words` an explicit type: it even writes one, Vec<_>, where the _ means "work this part out yourself".

Fixes that were compiled for real

  • Say the type on the name — compiles. The most common way. <code>Vec&lt;&amp;str&gt;</code>: a list of slices pointing into <code>said</code>, so no text is copied.
  • Say it on collect itself: ::<> — compiles. The type goes in <code>::&lt;…&gt;</code> right after the method name, nicknamed the turbofish because <code>::&lt;&gt;</code> looks a little like a fish. <code>_</code> means "work this part out yourself". Handy in the middle of a longer expression, where there is no name to annotate.
  • Trap: ask for a number — still E0277. A type must be something a chain can be collected into. A <code>usize</code> is a single number, not a collection; to get how many words there are, end the chain with <code>count()</code> instead.

Chatterbox v0.6: /find, /count and /stats as chains

Here is the whole program after this lesson, new lines in green. Two commands are rewritten as chains, and one is new.

  • /count becomes one line: history.iter().map(|m| m.text.len()).sum(). The type on bytes: usize tells sum() what kind of number to add up, the same job the annotation did for collect().
  • /find asks the same question as lesson 6.4, "yours, and contains the word?", but no longer keeps it in a let is_match: the closure is written straight inside filter(|m| …), where it is used. It still captures word by borrow, and here it needs no type on m, because filter already knows what it is walking. collect() gathers the hits into a Vec<&Message>: borrows pointing into the history, nothing copied. If that Vec is empty, Chatterbox says "you never said …"; otherwise a for loop prints each hit.
  • /stats is new. It walks history.iter(), keeps your messages with filter, and splits each into words with flat_map(|m| m.text.split_whitespace()). A for loop consumes that chain and counts each word, lowercased so that "Rust" and "rust" are one word, with entry and or_insert from lesson 6.3.
  • Then counts.into_iter().collect() turns the map into a Vec<(String, u32)> of pairs. It is into_iter on purpose: the map is not needed again, so its Strings move into the Vec instead of being copied.
  • sort_by sorts with a closure that is handed two pairs, a and b, and must say which goes first. It says so with cmp, which compares two values and answers one of three words: Less, Equal or Greater. (Those three words are a type called Ordering; the other value goes in as a borrow, hence the &.) .1 is the second part of a pair, the count, and .0 the first, the word (lesson 2.6). b.1.cmp(&a.1) compares the counts with b first, so the biggest count comes first; when they are Equal, .then(a.0.cmp(&b.0)) asks a second question, the words in alphabetical order, so the answer never changes from one run to the next.
  • Finally take(3) keeps the top three, map(|(word, n)| …) takes each pair apart (lesson 5.5) and turns it into text like rust (2), and collect() builds a Vec<String> to join. No words at all means "nothing said yet".
Chatterbox v0.6 after lesson 6.5: the whole program (green lines are new in this lesson)
src/main.rs37 new lines
// 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);
}
You type
Sam
rust is fun
Rust is fast
tea
/stats
/quit
Chatterbox prints (a real run)
Chatterbox v0.6
What should I call you?
Hi Sam! I'm Chatterbox. Ask me anything.
bot: you said: rust is fun
bot: you said: Rust is fast
bot: you said: tea
bot: your top words: is (2), rust (2), fast (1)
bot: bye, Sam!
messages so far: 3

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

And before you have said anything:
You type
Sam
/stats
/quit
Chatterbox prints (a real run)
Chatterbox v0.6
What should I call you?
Hi Sam! I'm Chatterbox. Ask me anything.
bot: nothing said yet
bot: bye, Sam!
messages so far: 0
Going deeper How sort_by uses cmp

cmp on its own is a question with three possible answers. 2.cmp(&5) is Less, 5.cmp(&5) is Equal, 7.cmp(&5) is Greater; for text, "fun".cmp("rust") is Less, because f comes before r in the alphabet. sort_by asks your closure that question about two items at a time, as often as it needs to: Less means "a goes before b", Greater means "b goes before a", and Equal means "either way", in which case the two keep the order they already had.

Swapping the two sides turns the order around. a.1.cmp(&b.1) would put the smallest count first; b.1.cmp(&a.1) puts the biggest first. .then(…) is only consulted when the first answer is Equal. So sorting ("tea", 1), ("rust", 2) and ("fun", 2) this way gives ("fun", 2), ("rust", 2), ("tea", 1): the two counts of 2 come first, and between them the alphabet decides.

Going deeper Is a chain slower than a loop?

Usually not. Because the compiler can see the whole chain at once, it can usually turn it into the same machine code as the loop you would have written by hand, so the shorter version costs nothing extra when the program runs. The Rust Book calls iterators a zero-cost abstraction, and measures one example in its chapter on loops versus iterators.

Try itYour turn: every word you said

The first half of /stats, on its own. Write words_said: every word in your messages (author you, not the bot), in the order you said them, in lower case. Write it as one chain from history.iter() to collect(). As in the other chapter 6 exercises, the history comes in as &[Message], a borrowed view of the list.

The first half of /stats, on its own. Write words_said: every word in your messages (author you, not the bot), in the order you said them, in lower case. Write it as one chain from history.iter() to collect(). As in the other chapter 6 exercises, the history comes in as &[Message], a borrowed view of the list.

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 words_said(history: &[Message]) -> Vec<String> {
// your words, in order, in lower case
Vec::new()
}
 
fn main() {
let history = vec![Message::new("you", "Rust is fun"), Message::new("bot", "you said: Rust is fun")];
println!("{:?}", words_said(&history));
}

Hidden tests (4): empty_history_has_no_words extra_spaces_make_no_empty_words words_in_order_and_lower_case the_bot_does_not_count

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

What you have now

An iterator is a small value with a next(). iter, iter_mut and into_iter decide whether each step hands you a look, a change or the item itself, by the rules you learned in chapter 4 for handing values over. Adapters wrap iterators without doing anything; a consumer at the end pulls each item through the whole chain. Chapter 6 is done: Chatterbox keeps a history it can number, count, search and summarise, and facts it can remember by name. Next it learns to say clearly when something goes wrong.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does <code>next()</code> return, and when does a <code>for</code> loop stop?Reveal
Some(item) while there are items, then None. A for loop calls next() until the first None.
Which iterator do you use to add a <code>!</code> to every String in a Vec, and why?Reveal
iter_mut(): each step hands out an exclusive borrow, so you may change the item and the Vec stays yours.
Why does <code>history.iter().map(…)</code> on its own print nothing?Reveal
Adapters are lazy. They only build a bigger iterator; nothing runs until a consumer such as sum, collect, count or a for loop asks for items.
How do you tell <code>collect()</code> what to build?Reveal
Give the name a type, as in let words: Vec<&str>, or write it on the call: collect::<Vec<_>>().

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

Questions people ask

What is an iterator in Rust?

A value that hands out items one at a time. Each call to next returns Some with the next item, or None when there are no more. A for loop is a loop that keeps calling next.

What is the difference between iter, iter_mut and into_iter?

iter hands out shared borrows, so you can only look. iter_mut hands out exclusive borrows, so you can change each item. into_iter hands out the items themselves and uses up the collection.

Are Rust iterators lazy?

Yes. map, filter and the other adapters do nothing when you write them. The work happens one item at a time when a consumer such as collect, sum, count or a for loop asks for items.

Why does collect need a type annotation?

collect can build many kinds of collection, such as a Vec, a String or a HashMap, and the chain alone does not say which. Give the variable a type, or write the type on the call itself with the turbofish syntax, two colons and the type in angle brackets right after collect.

Are iterator chains slower than loops in Rust?

Usually not. The compiler sees the whole chain and generally produces the same machine code as an equivalent hand-written loop. The Rust Book calls iterators a zero-cost abstraction.

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