Iterators, HashMap, and your first generic function

Lesson 0008 · after 0007 · reading, then a 35-minute drill against a shipped test file · ~50 minutes

Every code block, every compiler message, and every terminal session on this page was produced by running it today. Nothing is written from memory. The demo domain is a library shelf, defined in full in the next section — your project is a task CLI, so nothing here pastes in. Translating is the work.

The demo domain, in full

Every example on this page runs against the same four books, so it is worth reading the data model once before the examples start. Then any snippet below can be read without guessing what a field is called or what type it holds. This is the whole thing — two type definitions, two helpers, and one Vec:

use Shelf::*;   // so the examples can say Fiction instead of Shelf::Fiction

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
enum Shelf { Fiction, History, Poetry }        // fieldless, so Copy is free

#[derive(Debug)]
struct Book {
    title: String,        // owned text
    shelf: Shelf,         // which shelf it belongs on
    borrowed: bool,       // is it out on loan right now
}

fn book(title: &str, shelf: Shelf, borrowed: bool) -> Book {
    Book { title: title.to_string(), shelf, borrowed }
}

// "fiction" -> Some(Fiction), anything unknown -> None
fn shelf_of(word: &str) -> Option<Shelf> {
    match word {
        "fiction" => Some(Fiction),
        "history" => Some(History),
        "poetry"  => Some(Poetry),
        _ => None,
    }
}

// The data. Four books, three shelves, one of them out on loan.
let mut shelf: Vec<Book> = vec![
    book("Dubliners", Fiction, true),
    book("SPQR",      History, false),
    book("Ariel",     Poetry,  false),
    book("Beloved",   Fiction, false),
];

// A second value, used only where an element must be a plain string:
let titles: Vec<&str> = vec!["Dubliners", "SPQR", "Ariel"];

Two naming conventions to hold on to, because they are the only way to know an element's type at a glance. shelf (lowercase) is the Vec<Book>, so shelf.iter() hands you &Book and the closure parameter is written |b|. Shelf (capitalised) is the enum, so b.shelf is a field holding one of its three variants. And titles is a Vec<&str>, so titles.iter() hands you &&str and the closure parameter is written |t|.

The mapping onto your own crate is exact, which is what makes the translation mechanical rather than creative: Book is Task, title is title, Shelf is Priority, and borrowed is Status. So when a snippet below counts books per shelf, you are reading the count_by_priority you are about to write.

Where 0007 left you, and one prediction I got wrong

Thirty-two tests are green and the persistence layer behind them is real: fs::read_to_string and fs::write, a NotFound match guard so a first run does not look broken, an impl FromStr for Task with its own associated type Err, and a hand-written PartialEq because io::Error refuses to have one. The best signal is in command.rs: both hand-rolled match id.parse() blocks are gone, replaced by .parse()?. That was the whole point of step 0, and it landed — ? plus From is now a reflex rather than a fact.

But I predicted something in 0007 that turned out to be false, and it is worth a paragraph because the lesson generalises. I claimed the compiler would force you to write impl From<io::Error> for TaskError, because ? on fs::write would be the only reasonable shape. You wrote this instead:

fs::write(path, contents).map_err(TaskError::Io)

That is perfectly good Rust. TaskError::Io is a tuple-variant constructor, which means it is also a function of type fn(io::Error) -> TaskError, so handing it straight to map_err is idiomatic and allocation-free. No From impl needed, no error, and the test still passes. My claim was simply wrong: a compiler error can only force a design when no legal alternative exists, and here a legal alternative existed.

So which one should you write? Both are correct, and the difference is leverage rather than style. map_err converts at one call site; From converts at every call site, including ones you have not written yet, and it is what makes bare ? work on any function in std::fs, std::io, or a future crate that returns an io::Error. Today's save gets rewritten anyway, and the rewrite is shorter when ? just works — so step 0 of the drill writes the impl and deletes the map_err. One line each way.

Part 1 — An iterator is a lazy machine with one button

You have written iterator code already, in bursts: env::args().skip(1).collect() in main, self.tasks.iter().find(|t| t.id == id) in find, and .map(|t| t.id).max().unwrap_or(0) in load. What you have not had yet is the model underneath them, so each one was memorised separately. The model is unusually small — one trait, one method:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ~75 more methods, all with default bodies built on next()
}

Book: 13.2 — Processing a series of items with iterators · std: Iterator

That is the entire interface. next hands back Some(item) until the sequence runs out, then None forever. Notice type Item: it is an associated type, the same mechanism you filled in as type Err when you implemented FromStr last lesson. Every other method — map, filter, find, collect, sum — is a default method written in terms of next. Which is why learning the vocabulary is cheap: there is no new machinery behind any of them, only different ways of pressing the same button.

The one property that trips everybody up is laziness. The book states it flatly:

In Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up.

Book: 13.2

Building a chain of adapters does no work and touches no elements. It only describes work. Write a chain and forget to finish it, and the closure never runs even once — the compiler warns, because the warning is the only thing standing between you and a silently dead line of code:

titles.iter().map(|t| t.to_uppercase());
warning: unused `Map` that must be used
 --> examples/e1.rs:3:5
  |
3 |     titles.iter().map(|t| t.to_uppercase());
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default

So every chain has exactly two parts, and it is worth naming them because the names tell you where a chain must end. Adapters take an iterator and return another iterator: map, filter, enumerate, skip, take, rev. They are lazy, and they compose. Consumers take an iterator and return something that is not an iterator: collect, find, position, count, sum, max, any, for_each. They do the work, and a chain that does not end in one has not run.

The performance question answers itself once you see the structure, and it matters for the job you are aiming at. A chain of adapters is not a chain of temporary vectors — each adapter is a small struct wrapping the previous one, and the whole tower compiles down to a single pass. That is why rewriting a for loop as an iterator chain costs nothing at runtime, and why nobody in Rust treats the choice as a speed trade-off.

Part 2 — Three ways to iterate, and the ownership behind each

Before any adapter runs you have to say how you want the elements, and this is the one place where iterators meet the borrow rules. There are three methods, they differ only in the ownership they hand out, and picking the wrong one is the most common way an iterator chain fails to compile:

CallItem typeUse it when
v.iter()&TYou are reading. The collection survives.
v.iter_mut()&mut TYou are editing in place. It survives.
v.into_iter()TYou want the elements out. It is consumed.

Book: 13.2 — “if we want to create an iterator that takes ownership … we can call into_iter”

This is the same three-way choice you already make with &self, &mut self, and self in a method signature, applied one element at a time — so nothing new is being introduced, only a new place for a rule you already know. It also explains a piece of your own code you may have written without reading: your complete uses iter_mut because it assigns to task.status, while find uses iter because it only looks. Swap them and neither compiles.

One trap deserves seeing before you hit it in the drill, because the error message is about a borrow and the cause is an iterator. When you keep the result of an iter_mut chain in a variable, the mutable borrow of the whole collection stays alive for as long as that variable does:

struct Library { books: Vec<String> }   // titles only, to keep the error bare

impl Library {
    fn rename(&mut self, from: &str, to: &str) {
        let book = self.books.iter_mut().find(|b| *b == from).unwrap();
        println!("{} books", self.books.len());   // asks for a second borrow
        *book = to.to_string();
    }
}
error[E0502]: cannot borrow `self.books` as immutable because it is also borrowed as mutable
 --> examples/e4.rs:5:44
  |
4 |         let book = self.books.iter_mut().find(|b| *b == from).unwrap();
  |                    ---------- mutable borrow occurs here
5 |         println!("renaming 1 of {} books", self.books.len());
  |                                            ^^^^^^^^^^ immutable borrow occurs here
6 |         *book = to.to_string();
  |         ----- mutable borrow later used here

Read the three annotations as a timeline and the rule falls out: the borrow begins at iter_mut(), and it ends after the last use of book, not at the end of the statement that created it. Anything else touching self.books in between is a second borrow, and that is exactly the rule from chapter 4. The fix is to reorder — read the length first, or finish with book before asking. Nothing about iterators is special here; they just make the overlap easy to write by accident.

Part 3 — The verbs you will use every day

Here is the whole working vocabulary, run against a shelf of books. Read the calls beside their real output rather than trying to memorise signatures — the shapes are what you want in your fingers:

// the same four books from the top of the page
let mut shelf: Vec<Book> = vec![
    book("Dubliners", Fiction, true), book("SPQR", History, false),
    book("Ariel", Poetry, false),     book("Beloved", Fiction, false),
];

let all_titles: Vec<&str> = shelf.iter().map(|b| b.title.as_str()).collect();
shelf.iter_mut().for_each(|b| b.borrowed = false);

let fiction: Vec<&str> = shelf.iter()
    .filter(|b| b.shelf == Shelf::Fiction)
    .map(|b| b.title.as_str())
    .collect();

shelf.iter().find(|b| b.title == "Ariel").map(|b| b.shelf);
shelf.iter().position(|b| b.title == "Ariel");
shelf.iter().any(|b| b.borrowed);               // bool
shelf.iter().filter(|b| !b.borrowed).count();   // usize
shelf.retain(|b| b.shelf != Shelf::Poetry);     // delete in place
all_titles  -> ["Dubliners", "SPQR", "Ariel", "Beloved"]
iter_mut    -> every book returned, borrowed set to false on each
fiction     -> ["Dubliners", "Beloved"]
find        -> Some(Poetry)        // the ITEM,  mapped: Option<Shelf>
position    -> Some(2)             // the INDEX: Option<usize>, Ariel is 3rd
any / count -> false / 4           // nothing is borrowed now, so all 4 are in
retain      -> removed 1, 3 left   // Ariel was the only poetry book

Two of those are worth a second look, because they are the ones your own store.rs currently writes out longhand as loops.

find versus position is a question about what you need next. find gives you the element, which is what complete wants — it has to assign to status. position gives you the index, which is what remove wants — Vec::remove takes an index, not an element. Both return an Option, and both compose straight into the error handling you already have: .ok_or(TaskError::NotFound(id))? turns a None into your own error and unwraps the rest, collapsing a nine-line loop into one expression.

retain is the one that saves you from a genuine bug. Deleting several elements from a Vec by index in a loop is a classic error: each removal shifts everything after it down one, so the loop skips elements. retain takes a predicate meaning “keep this one” and does a single compacting pass. It is a method on Vec rather than on Iterator, because it mutates the collection in place — one of several useful methods that live on the collection rather than on the trait.

std: Vec::retain · Iterator::position

Part 4 — collect is the interesting one

collect looks like “make a Vec”, and that undersells it enough to hide the single most useful trick in this lesson. Its real signature says something much stronger:

fn collect<B: FromIterator<Self::Item>>(self) -> B

std: Iterator::collect · FromIterator

Read that as: collect will build any type that knows how to be built from this kind of item. The target is chosen by B, and B is decided by you, at the call site — which is why collect is the one method where you routinely have to state a type. Leave it out and the compiler has nothing to go on:

let shouted = titles.iter().map(|t| t.to_uppercase()).collect();
error[E0283]: type annotations needed
    --> examples/e2.rs:3:9
     |
   3 |     let shouted = titles.iter().map(|t| t.to_uppercase()).collect();
     |         ^^^^^^^                                           ------- type must be known at this point
     |
     = note: multiple `impl`s satisfying `_: FromIterator<String>` found in the `alloc` crate:
             - impl FromIterator<String> for Box<str>;
             - impl FromIterator<String> for String;

The note is the teaching. This is not the compiler being fussy about vectors — it is telling you that several types can be built from a stream of Strings and it will not guess which one you meant. You answer either on the left, let shouted: Vec<String> = ..., or on the right with a turbofish, .collect::<Vec<String>>(). Both are common; pick whichever reads better in the line.

Now the trick. Result and Option both implement FromIterator, so an iterator of Results can collect into a single Result holding a Vec. The same chain, with only the target type changed, gives two entirely different answers:

let words = ["fiction", "history", "rubbish", "poetry"];

let each: Vec<Option<Shelf>> = words.iter().map(|w| shelf_of(w)).collect();
let all:  Option<Vec<Shelf>> = words.iter().map(|w| shelf_of(w)).collect();

let numbers: Result<Vec<u32>, _> =
    "1 2 x 4".split(' ').map(str::parse::<u32>).collect();
Vec<Option> -> [Some(Fiction), Some(History), None, Some(Poetry)]
Option<Vec> -> None
Result<Vec> -> Err("invalid digit found in string")

std: impl FromIterator<Result<A, E>> for Result<V, E>

Vec<Option<Shelf>> keeps every outcome, hole included. Option<Vec<Shelf>> means all-or-nothing: the first None ends the iteration and the whole result is None. That short-circuit is not a detail — it is the reason this is the right tool for reading a file. Your load currently loops, parses each line, and pushes into a Vec, with ? inside the loop. One collect replaces all of it:

let tasks: Vec<Task> = contents.lines()
    .map(str::parse)
    .collect::<Result<Vec<Task>, TaskError>>()?;

The behaviour is exactly what a save file wants. Every line parses and you get the tasks; one line is corrupt and you get that line's error and nothing else — no half-loaded store to accidentally save back over the good file. And notice map(str::parse): you can pass a function path where a closure is expected, because |line| line.parse() and str::parse are the same function. Which parse, of the many possible, is settled by the collect target — the Vec<Task> tells the compiler to look for Task's FromStr impl, the one you wrote last lesson.

One line-splitting detail comes with this rewrite, and it is a real trap rather than trivia:

let file = "1|fiction|Dubliners\n2|poetry|Ariel\n";
file.split('\n')   // -> ["1|fiction|Dubliners", "2|poetry|Ariel", ""]
file.lines()       // -> ["1|fiction|Dubliners", "2|poetry|Ariel"]

std: str::lines

split('\n') yields an empty final piece for a file that ends in a newline, because the text after the last separator is the empty string. That is why your current load needs an if !items.is_empty() guard — the guard exists to paper over the wrong splitter. lines() is built for this job: it treats the trailing newline as a terminator rather than a separator, and it strips a \r\n too, which is free Windows compatibility. Switch splitters and the guard disappears — after which a blank line in the middle of a file is no longer silently skipped but reported as a bad line, which is the honest answer for a corrupt file. A shipped test pins that behaviour.

Part 5 — HashMap, and the two traits a key must have

HashMap<K, V> is the last of the three common collections, and it is the one you have not used at all. It is a lookup by key rather than by position, it lives on the heap like Vec, and it is not in the prelude, so it needs an import:

use std::collections::HashMap;

let mut counts: HashMap<Shelf, usize> = HashMap::new();
counts.insert(Shelf::Fiction, 2);
counts.get(&Shelf::Fiction);              // Option<&usize> — may be absent
counts.get(&Shelf::Poetry).copied().unwrap_or(0);   // absent counts as 0
for (shelf, n) in &counts { }             // ARBITRARY order — never trust it

Book: 8.3 — Storing keys with associated values in hash maps

Two things there are easy to skim past and expensive to learn later. get returns an Option<&V>, so “missing key” is a value you handle rather than a crash — the same shape as Vec::get. And iteration order is arbitrary and not stable between runs. If a user is going to read your output, you must impose an order yourself; the stats command in today's drill prints high, medium, low in a fixed sequence for exactly that reason.

The idiom that makes hash maps worth their weight is entry. Counting things is the standard example, and the book's version is four lines:

for b in &shelf {
    *counts.entry(b.shelf).or_insert(0) += 1;
}
entry() -> {Fiction: 2, History: 1}

Book: 8.3 — Listing 8-25, counting occurrences of words

Take that line apart slowly, because it is dense and it is everywhere in real Rust. entry(key) returns an Entry, an enum standing for a slot that may or may not be filled. or_insert(0) fills it with 0 if it was empty, and either way hands back a &mut usize pointing into the map. * follows that reference so += 1 lands on the number itself. The whole thing is one hash lookup — the version you would write by hand, if !map.contains_key(k) { map.insert(k, 0) } followed by a get_mut, costs two or three and reads worse.

Now the part that is specific to Rust. A key type must implement Eq and Hash, and you will meet that rule at a call site from today's drill — so here is the line the next two errors point at, before they point at it. Store::count_by_priority is one line long, and it hands the work to a small generic function called tally. You write both in the drill; Part 6 builds tally from this signature:

// src/stats.rs — Part 6 explains it and the drill writes the body
pub fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>

// src/store.rs:68 — count_by_priority, in full
tally(self.tasks(), |task| task.priority)

Read that as “count the items, grouped by whatever the closure pulls out of each one”. It is all you need for the errors below; the three type parameters and the body are Part 6's job. Your Priority implements neither Eq nor Hash, so the first attempt at using it as a key fails twice over:

error[E0277]: the trait bound `Priority: Eq` is not satisfied
  --> src/store.rs:68:9
   |
68 |         tally(self.tasks(), |task| task.priority)
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Eq` is not implemented for `Priority`
   |
help: consider annotating `Priority` with `#[derive(Eq)]`

error[E0277]: the trait bound `Priority: Hash` is not satisfied
help: consider annotating `Priority` with `#[derive(Hash)]`

The requirement is not bureaucracy; it is the data structure stating its contract. To find a key the map hashes it to pick a bucket, then compares for equality inside that bucket — so a key it cannot hash or cannot compare is a key it cannot store. Hash gives it the first, Eq the second.

Eq is worth understanding rather than just deriving, since you already have PartialEq and this looks like a duplicate. It is not: Eq is a marker with no methods of its own, and it promises one extra property that PartialEq does not — that every value equals itself. The famous exception is f64, where NAN != NAN, which is precisely why f64 implements PartialEq but not Eq, and why a f64 cannot be a HashMap key. A three-variant enum has no such problem, so the derive is honest.

std: Eq · Hash

Fix those two and a third error appears, which is the most instructive of the set:

error[E0507]: cannot move out of `task.priority` which is behind a shared reference
  --> src/store.rs:68:36
   |
68 |         tally(self.tasks(), |task| task.priority)
   |                                    ^^^^^^^^^^^^^ move occurs because `task.priority` has type `Priority`,
   |                                                  which does not implement the `Copy` trait
   |
note: if `Priority` implemented `Clone`, you could clone the value

The closure receives &Task — a borrow — and a map key has to be owned, since the map keeps it. Reading task.priority out of a borrow is a move out of something you do not own, which is E0507, one of the most common errors in real Rust. Three fixes exist and they are not equivalent: .clone() works but is noise for three variants; #[derive(Clone, Copy)] makes Priority behave like u32, copied implicitly wherever it is read; keying by task.priority.label() sidesteps it by using a &str instead. Derive Copy. A fieldless enum is a single small integer at runtime, copying it is free, and it is what std does for its own small enums such as ErrorKind.

Part 6 — Your first generic function

Counting tasks by priority is one specific job, and today you will write it once and never again — because the function you write is generic over what it counts. This is your first hand-written generic, so here it is whole, and then taken apart:

use std::collections::HashMap;
use std::hash::Hash;

pub fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
where
    K: Eq + Hash,
    F: Fn(&T) -> K,
{
    let mut counts = HashMap::new();
    for item in items {
        *counts.entry(key(item)).or_insert(0) += 1;
    }
    counts
}

Three type parameters, and each one is there for a reason. T is the element type, and the function never looks inside a T, which is exactly why it works on tasks and on strings alike. K is the key type, and it carries the bound Eq + Hash — not because tally cares, but because the HashMap it returns does. F is the closure type. Every closure in Rust has its own anonymous type, so the only way to accept one is a type parameter bounded by Fn(&T) -> K, which reads as “anything callable that takes a &T and returns a K”.

The where clause is worth seeing as the point of the exercise rather than syntax to tolerate. It is a contract in both directions: callers must supply types that satisfy it, and inside the body you may use exactly the operations it guarantees and nothing else. That is why generics in Rust do not blow up at the call site the way C++ templates can — the bounds are checked once, against the definition. Try to call item.to_string() in there and it will not compile, because nothing in the clause promised T: Display.

The payoff is that one definition serves cases that have nothing to do with each other:

tally(&shelf, |b| b.shelf)          // -> {History: 1, Fiction: 2}
tally(&["a", "bb", "cc"], |w| w.len())   // -> {1: 1, 2: 2}

Two calls, two different T, two different K, and — this is the part that matters for the interviews you are aiming at — no runtime cost for the generality. Rust monomorphises: it compiles one specialised copy of tally per combination of types actually used, so each call site gets code as tight as if you had written that version by hand.

Book: 10.1 — Generic data types and 13.1 — Closures

Last note before the drill, and it is a taste question rather than a rule. tally's body keeps a for loop, on purpose. Iterators replace loops that search, transform, or collect — those have a named adapter and the chain reads better than the loop. A loop that folds many items into one accumulator is the case where a loop is still the clearest thing to write; the iterator version exists, fold, and here it would be harder to read for no gain. The drill's grep check is scoped to store.rs for exactly this reason.

Check yourself before the drill

Six questions before you touch the keyboard. Answer each one out loud, in full sentences, before you reveal or click. An answer you can say is an answer you have understood; one you can only recognise on the page usually is not. Getting one wrong here costs nothing — getting it wrong twenty minutes into the drill costs you the drill.

Iterators

How many times does the closure run in v.iter().map(|x| f(x)); — with no collect?

Iterators

complete needs the task itself; remove needs its index. Which adapter does each want, and which iterator does each start from?

Collections

Four lines are parsed, the third is corrupt, and you collect::<Result<Vec<Task>, TaskError>>(). What comes back?

Collections

Why must a HashMap key implement both Eq and Hash, and why is PartialEq not enough?

Ownership

|task| task.priority in a closure over &Task gives error[E0507]: cannot move out of ... behind a shared reference. What is the cause, and which of the three fixes wins?

Traits

In fn tally<T, K, F>(items: &[T], key: F) with K: Eq + Hash, F: Fn(&T) -> K — why does F have to be a type parameter at all, and what does the where clause buy you?

The drill — 35 minutes, your own crate

Type it, do not paste it. The bookshelf above is a different program. Keep the iterators reference open — looking syntax up is free.

cd ~/learn-rust/tasks
cp ../lessons/0008-collections-spec.rs tests/collections.rs
cargo test            # 14 new tests fail to compile — that is the starting line

Do not edit anything in tests/. All 32 existing tests must still pass. Target at the end: 46 passing.

Step 0 — the second From, two minutes

Write the impl that 0007 asked for and then delete the workaround, so ? handles io errors everywhere from here on:

impl From<io::Error> for TaskError { .. }   // in error.rs, beside the other
fs::write(path, contents)?;                 // in save — map_err goes away

Check: grep -c "impl From<io::Error>" src/error.rs prints 1, grep -c "map_err(TaskError::Io)" src/store.rs prints 0, and cargo test --test persist still passes 8.

Step 1 — a new module and one generic function

Create src/stats.rs, declare it in lib.rs, and write tally from Part 6 — from the signature, not by copying the body. It is nine lines.

Check: cargo test --test collections tally → 3 passed.

Forgotten how a module is declared?

pub mod stats; in src/lib.rs, alphabetically beside the others. Without that line the file is not compiled at all and you get error[E0432]: unresolved import from the test file — the same error 0006 showed you.

Step 2 — Priority as a key

Add count_by_priority(&self) -> HashMap<Priority, usize> to Store, as one line delegating to tally. Let it fail first, read all three errors, and fix them with the derives they ask for. Seeing E0277 twice and E0507 once, in that order, is the point of the step.

Check: cargo test --test collections count_by → 3 passed.

Step 3 — two more methods on Store

pub fn titles_with(&self, priority: Priority) -> Vec<&str>
pub fn remove_completed(&mut self) -> usize

titles_with answers “what am I meant to be doing at this priority?”. Hand it a priority and it gives back the title of every task that carries that priority, in the order the tasks were added. Nothing matches, and you get an empty Vec rather than an error — an empty answer is a legitimate answer here. Note the return type: Vec<&str>, not Vec<String>. It hands back borrows of titles the store still owns, so nothing is cloned, and task.title.as_str() is the conversion you need.

remove_completed is the tidy-up: it deletes every task whose status is Done and returns how many it deleted. Three details the tests hold you to. The tasks that survive keep their own ids — you are removing rows, not renumbering them. The id counter is untouched, so the next add carries on from where it had got to rather than reusing a freed number. And removing nothing is a normal outcome that returns 0, not an error. One Vec method from Part 3 does the removal; the count is the length before minus the length after.

Check: cargo test --test collections → 11 of 14 passed.

Step 4 — rewrite store.rs with what you learned

Four functions, all currently loops, all one expression each: complete with iter_mut().find(), remove with iter().position(), save with map(..).collect::<String>(), and load with lines().map(str::parse).collect::<Result<Vec<Task>, TaskError>>()?. In load the if !items.is_empty() guard goes away with the splitter, and the trailing mut contents = String::new() dance collapses into the match from 0007 returning a value.

Check: grep -c "for " src/store.rs prints 0, grep -c "lines()" src/store.rs prints 1, and cargo test → 17 + 7 + 8 + 14 = 46 passed.

Stuck on save building a String from an iterator?

String implements FromIterator<String>, so a chain of owned lines collects straight into one: self.tasks.iter().map(|t| format!("{}\n", t.to_line())).collect(). Annotate the target — let contents: String = .. — or E0283 will ask you which of several possible types you meant.

Step 5 — two new commands, so the CLI shows it

Add Stats and Clear to the Command enum and to Command::parse (the words are stats and clear). The match in main will refuse to compile until both are handled — that is E0004, the same non-exhaustive-match error from 0006, doing its job again.

stats must print the three priorities in a fixed order, because hash map iteration order is arbitrary. Loop over [Priority::High, Priority::Medium, Priority::Low] and ask the map for each, with counts.get(&p).copied().unwrap_or(0) so an absent priority prints 0 rather than vanishing.

Check — a real session, run today against the reference implementation:

$ cd $(mktemp -d)
$ run add "buy milk" high
added task 1
$ run add "call bank"
added task 2
$ run add "water plants" low
added task 3
$ run done 1
completed 1
$ run stats
high   1
medium 1
low    1
$ run clear
cleared 1 completed
$ run list
2 [todo] call bank (medium)
3 [todo] water plants (low)
$ cat t.txt
2|todo|medium|call bank
3|todo|low|water plants
$ run stats ; echo $?
high   0
medium 1
low    1
0

(run above is TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml --.) That last high 0 is .copied().unwrap_or(0) earning its place: the completed high-priority task is gone, so the map has no High entry at all, and the absence prints as a zero instead of a missing line.

Then stop

Not today: fold and zip, BTreeMap (sorted keys — the right answer if you ever want stats ordered without hard-coding), impl Iterator for your own type, and itertools. Each is a small step from here, and none of them is on the path to the next gap.

What this closed

Chapters 8 and 13 move to produced on the coverage map, and 10.1 opens with a real generic function of your own rather than a book example. What is left before the job-ready floor is short:

  1. ch 11 — writing your own tests (lesson 0009). You have now consumed 46 of my tests and written zero. Test-writing is a first-round interview question, and it is the last big gap in the book's core.
  2. ch 10.3 — lifetimes, as reading practice. You wrote one today without noticing: titles_with returns Vec<&str> borrowed from &self, and elision filled in the annotation for you.
  3. Then serde → axum, where the trait work from 0005–0008 starts paying rent.

Take it outside

Here is a question with genuine disagreement behind it, which makes it a good one to ask people rather than docs. Your tally takes &[T]. Most experienced Rust developers would write it to take impl IntoIterator<Item = T> instead, so it accepts a Vec, an array, a HashSet, or any chain of adapters — not only a slice. Post tally on users.rust-lang.org (Code Review category) and ask whether the IntoIterator version is worth the extra signature complexity for a small crate, and where they personally draw that line. The answers will teach you more about idiomatic API design than any chapter, because it is a taste question and the book cannot have taste for you.

The five sentences worth keeping

  1. Adapters are lazy and return iterators; consumers do the work. A chain that does not end in a consumer never ran.
  2. iter borrows, iter_mut borrows mutably, into_iter takes ownership — the &self/&mut self/self choice, one element at a time.
  3. collect builds any FromIterator type, so an iterator of Results collects into one Result<Vec<_>, E> that short-circuits on the first error.
  4. A HashMap key needs Eq + Hash; *map.entry(k).or_insert(0) += 1 is the counting idiom; iteration order is arbitrary, so impose your own before printing.
  5. A generic function's where clause is a contract checked once against the definition, and monomorphisation means the generality is free at runtime.