19.xChapter 19 summary and quiz
This chapter handed you the tools that make day-to-day Rust expressive. Review, then a capstone that uses most of them at once.
Quick review
A closure is a function written inline that can capture variables from its surrounding scope (19.1). It captures by the lightest means that compiles, & to read, &mut to modify, or by value, and move forces every capture to be owned (needed when the closure must outlive the originals). A closure's parameter and return types are inferred from use, then fixed.
Each closure has a unique anonymous type, so you name closures in signatures through the Fn traits (19.2): Fn (reads captures, callable repeatedly), FnMut (mutates captures), FnOnce (may consume captures, callable once). They nest Fn ⊂ FnMut ⊂ FnOnce; a parameter should ask for the most permissive bound it needs. Take a closure with impl Fn(...) -> ... and return one with impl Fn plus move. Non-capturing closures and named functions also fit the fn pointer type.
An iterator is anything implementing Iterator: one required method next(&mut self) -> Option<Item>, returning Some per value and None when done (19.3). Get one from a collection with iter() (&T), iter_mut() (&mut T), or into_iter() (T), the &/&mut/owned choice again. Iterators are lazy: they do nothing until consumed.
Adapters transform one iterator into another and are lazy and chainable (19.4): map, filter, enumerate, zip, rev, chain. Consumers drive the iterator to a result (19.5): collect (needs a target type, via annotation or turbofish), sum, count, fold (the general reduction), and the short-circuiting find, any, all. A for loop is sugar for IntoIterator::into_iter plus while let Some(x) = iter.next() (19.6), which is why for x in &v was v.iter() all along. Pipelines are a zero-cost abstraction, identical to hand-written loops in release builds (19.7); choose the style that reads clearest.
Quiz time
Question #1
When a closure captures a variable, how does the compiler decide whether to borrow it (&), borrow it mutably (&mut), or move it, and what does move override?
Show solution
The compiler picks the least invasive capture the body requires: & if it only reads the value, &mut if it modifies it, by value only if it must own it. move overrides this to force every capture to be by value (owned), regardless of how the body uses it, which you need when the closure outlives the originals (returned from a function, sent to a thread).
Question #2
Why must you specify a type for collect but rarely for count?
Show solution
collect can produce many different collection types from the same iterator (Vec, String, HashMap, ...), so the compiler needs you to name the target via a binding annotation or a turbofish (.collect::<Vec<_>>()). count always returns usize, so there's nothing to specify. It's the same missing-type situation as parse in lesson 5.6.
Question #3
Predict the output:
let v = vec![1, 2, 3, 4];
v.iter().filter(|x| **x > 1).map(|x| x * 10);
println!("done");Show solution
It prints only done. The adapter chain is never consumed, so it does nothing, no filtering, no mapping (the compiler also warns the iterator is unused). Adapters are lazy; without a consumer like collect, sum, or a for loop, the pipeline is inert. (The **x peels filter's & off iter's &i32.)
Question #4
Which Fn trait does each closure implement: |x| x + 1, |x| { vec.push(x) } (where vec is captured), and move |x| consume(captured_string, x)?
Show solution
|x| x + 1 captures nothing and only computes: Fn (and therefore also FnMut and FnOnce). |x| vec.push(x) mutates the captured vec: FnMut (and FnOnce), but not Fn. move |x| consume(captured_string, x), if consume takes captured_string by value, moves the captured String out when called, so it can run only once: FnOnce only.
Chapter capstone
Put the chapter to work. Given a block of text, produce a word-frequency report: the five most common words, lowercased, with their counts, ignoring punctuation. This touches closures, adapters, a consumer building a HashMap, and a small sort.
Write a program that, for the input below, prints each of the top five words and its count, most frequent first.
fn main() {
let text = "the cat sat on the mat. The dog sat on the log. \
the cat and the dog sat.";
// your code here
}Show solution
use std::collections::HashMap;
fn main() {
let text = "the cat sat on the mat. The dog sat on the log. \
the cat and the dog sat.";
// Split into words, lowercase, strip non-alphabetic edges, count.
let mut counts: HashMap<String, u32> = HashMap::new();
for word in text.split_whitespace() {
let cleaned: String = word
.chars()
.filter(|c| c.is_alphabetic())
.collect::<String>()
.to_lowercase();
if !cleaned.is_empty() {
*counts.entry(cleaned).or_insert(0) += 1;
}
}
// Sort entries by count, descending, and take the top five.
let mut ranked: Vec<(String, u32)> = counts.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1));
for (word, n) in ranked.iter().take(5) {
println!("{word}: {n}");
}
}the: 5
sat: 3
cat: 2
on: 2
dog: 2
(Words tied on count may appear in any order, since HashMap iteration order isn't fixed; the and sat are unambiguous, the rest are a three-way tie at 2.) The pieces: split_whitespace yields each word; a filter/collect pipeline strips punctuation (chars().filter(is_alphabetic).collect::<String>()); entry(..).or_insert(0) is the chapter-18 count-up idiom; into_iter().collect() turns the map into a sortable Vec; sort_by with a comparison closure orders by count descending (b.1.cmp(&a.1)); and take(5) is an adapter limiting the output. The cleaning could also be a map in a fully iterator-style version, the for loop here is a fine choice because the body does a side-effecting HashMap update, exactly the "procedure, not transformation" case from lesson 19.7.
You can now move data through expressive pipelines and bottle up behavior as values. Chapter 20 turns all of this outward: it builds a real command-line program, reading arguments and files and standard input, and the iterator and closure tools you just learned are exactly what make its text-search project read so cleanly.