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.
$ cargo run
Some("rust")
Some("is")
Some("fun")
NoneThree 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.
sizes measured with size_of on this compiler (64-bit), not quoted
| Type | Bytes | Layout |
|---|---|---|
Vec<String> | 24 | pointer (8 B) · capacity (8 B) · length (8 B) |
std::slice::Iter<String> | 16 | where it is now (8 B) · where the list ends (8 B) |
std::vec::IntoIter<String> | 32 | the 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 write | Each step hands you | Afterwards the Vec is |
|---|---|---|
history.iter() (or &history) | a borrow, &Message: look only | untouched |
history.iter_mut() (or &mut history) | an exclusive borrow, &mut Message: you may change it | changed, still yours |
history.into_iter() (or plain history) | the Message itself: you own it now | gone, 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:
$ cargo run
["hi!", "hello!"]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 saystrue.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 saysNone.
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?
What is the FIRST line this program prints?
looking at samchain builttotal 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.
This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.
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 →
type annotations needed: the compiler cannot work out whatwordsshould be.collect()could make a Vec, a String or several other things from the same words.type must be known at this point, undercollect: this is the call that has to know, because it is the one doing the building.the type must implement `FromIterator<&str>`: whatever you choose must be able to be built from a series of&stritems. (That is a trait, a named ability; traits get their own lesson in chapter 8.)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<&str></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>::<…></code> right after the method name, nicknamed the turbofish because <code>::<></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 onbytes: usizetellssum()what kind of number to add up, the same job the annotation did forcollect(). - /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 insidefilter(|m| …), where it is used. It still captureswordby borrow, and here it needs no type onm, becausefilteralready knows what it is walking.collect()gathers the hits into aVec<&Message>: borrows pointing into the history, nothing copied. If that Vec is empty, Chatterbox says "you never said …"; otherwise aforloop prints each hit. - /stats is new. It walks
history.iter(), keeps your messages withfilter, and splits each into words withflat_map(|m| m.text.split_whitespace()). Aforloop consumes that chain and counts each word, lowercased so that "Rust" and "rust" are one word, withentryandor_insertfrom lesson 6.3. - Then
counts.into_iter().collect()turns the map into aVec<(String, u32)>of pairs. It isinto_iteron purpose: the map is not needed again, so its Strings move into the Vec instead of being copied. sort_bysorts with a closure that is handed two pairs,aandb, and must say which goes first. It says so withcmp, which compares two values and answers one of three words:Less,EqualorGreater. (Those three words are a type calledOrdering; the other value goes in as a borrow, hence the&.).1is the second part of a pair, the count, and.0the 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 areEqual,.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 likerust (2), andcollect()builds aVec<String>tojoin. No words at all means "nothing said yet".
Sam rust is fun Rust is fast tea /stats /quit
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.
Sam /stats /quit
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.
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.
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.
Say it out loud, then reveal
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
sum, collect, count or a for loop asks for items.How do you tell <code>collect()</code> what to build?Reveal
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.
Go deeper
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.