Vibe Engines
YouTube

Lesson 6.2 · 9 min

Walking a list

A history is only useful if you can look through it: print every message, number them, add up their lengths. Walking a list is also where the one-owner rule shows up again, because a loop can either borrow the items or take them.

Adds to ChatterboxPrint the history.

Loop and keep the list

You have used for over a fixed array of lines. A Vec works the same way, with one question the array did not raise: after the loop, do you still have the list?

That depends on what you hand to for. If you give it the Vec itself, the loop takes the Vec: it becomes the owner and hands you the items one at a time. If you give it &history (a borrow, from chapter 4), the loop only looks at the items, and the Vec stays yours.

The loop took the list, so it is gone
struct Message {
author: String,
text: String,
}
 
fn main() {
let history = vec![Message { author: "sam".to_string(), text: "hi".to_string() }];
for m in history {
println!("{}: {}", m.author, m.text);
}
println!("{} messages", history.len());
}
error[E0382]: borrow of moved value: `history`
  --> src/main.rs:11:29
   |
 7 |     let history = vec![Message { author: "sam".to_string(), text: "hi".to_string() }];
   |         ------- move occurs because `history` has type `Vec<Message>`, which does not implement the `Copy` trait
 8 |     for m in history {
   |              ------- `history` moved due to this implicit call to `.into_iter()`
...
11 |     println!("{} messages", history.len());
   |                             ^^^^^^^ value borrowed here after move
   |
note: the `for` loop is desugared into a call to `std::iter::IntoIterator::into_iter`, which takes ownership of the receiver `self`, which moves `history`
  --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/iter/traits/collect.rs:312:17
help: consider iterating over a slice of the `Vec<Message>`'s content to avoid moving into the `for` loop
   |
 8 |     for m in &history {
   |              +

The compiler says "borrow of moved value: history". Read it as: you gave history to the loop, so it is not yours any more, and then you asked it for its length. The fix is one character. Write for m in &history and the loop borrows instead.

Borrow it with &, and the list survives
struct Message {
author: String,
text: String,
}
 
fn main() {
let history = vec![Message { author: "sam".to_string(), text: "hi".to_string() }];
for m in &history {
println!("{}: {}", m.author, m.text);
}
println!("{} messages", history.len());
}
$ cargo run
sam: hi
1 messages

Numbering with enumerate

history.iter() is the same borrowing walk written as a method. Add .enumerate() and each step hands you two things: the position and the item. Write them as a pair, (i, m). Positions start at 0, so print i + 1 for people.

Chains that wait

iter() does not do the walk. It returns an iterator: a thing that knows how to produce the next item when someone asks. You can add steps to it, for example .map(|m| m.text.len()), which says "turn each message into its length". The |m| … part is a tiny function written in place; as a preview, the lesson two from now explains it properly. For now read it as "given m, produce this".

Here is the surprising part. A chain of steps does nothing until something at the end asks for the results: .sum(), .count() or .collect(), or a for loop. Rust calls these steps lazy. Work happens when the answer is requested, not when the chain is written.

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 Building the chain with map does not run the little function at all. "chain built" prints first. Only when sum() asks for the answer does Rust walk the list, printing "looking at sam", then "looking at ada", then the total is 7.

Chatterbox v0.6: /history

Add a /history command that prints every message with a number, and finish by adding up every message's length with the lazy chain you just saw. Notice that both walks borrow, so the history is still there afterwards, and the program can keep taking new messages.

src/main.rs: Chatterbox v0.6
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,
Say(String),
Unknown(String),
}
 
fn parse(line: &str) -> Command {
match line {
"/help" => Command::Help,
"/quit" => Command::Quit,
"/history" => Command::History,
_ => match line.strip_prefix("/say ") {
Some(rest) => Command::Say(rest.to_string()),
None => Command::Unknown(line.to_string()),
},
}
}
 
fn main() {
let user = "sam";
let mut history: Vec<Message> = Vec::new();
let lines = ["/say hello there", "/say anyone home?", "/history", "/quit"];
for line in lines {
match parse(line) {
Command::Help => println!("commands: /help /history /say /quit"),
Command::Quit => break,
Command::History => {
for (i, m) in history.iter().enumerate() {
println!("{}. {}: {}", i + 1, m.author, m.text);
}
}
Command::Say(text) => {
history.push(Message::new(user, &text));
}
Command::Unknown(other) => println!("unknown command: {other}"),
}
}
let total: usize = history.iter().map(|m| m.text.len()).sum();
println!("{} messages, {} characters", history.len(), total);
}
$ cargo run
1. sam: hello there
2. sam: anyone home?
2 messages, 23 characters
Try itYour turn: the numbered list

Write numbered. For each message return one line in the form 1. sam: hello: the position starting at 1, the author, a colon and a space, then the text.

Write numbered. For each message return one line in the form 1. sam: hello: the position starting at 1, the author, a colon and a space, then the text.

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 numbered(history: &Vec<Message>) -> Vec<String> {
// one String per message: "1. sam: hello"
Vec::new()
}
 
fn main() {
let history = vec![Message::new("sam", "hello"), Message::new("ada", "hi")];
for line in numbered(&history) {
println!("{line}");
}
}

Hidden tests (4): empty_history_gives_no_lines history_is_still_usable_after one_message numbers_follow_the_order

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

What you have now

You can walk a history without losing it: for m in &history borrows, for m in history takes. You can number items with enumerate, and you know a chain such as iter().map(…).sum() only runs when its last step asks. Next: counting messages per user, which needs a different kind of container.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What is the difference between for m in history and for m in &history?Reveal
The first moves the Vec into the loop, so you cannot use it afterwards. The second borrows it, so the Vec is still yours when the loop ends.
What does enumerate give you at each step?Reveal
A pair: the position (starting at 0) and the item.
Why does history.iter().map(...) print nothing by itself?Reveal
Iterator chains are lazy. Nothing runs until a final step such as sum, count, collect or a for loop asks for results.

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

Questions people ask

What is E0382 in Rust?

Use of a moved value. You gave a value away (here, to a for loop) and then tried to use it again.

Should I write for m in &history or for m in history.iter()?

They do the same thing. Both borrow the items. The &history form is shorter and most common.

What does collect do?

It runs an iterator chain and gathers the results into a collection such as a Vec.

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