Rust Interview Mastery · Chapter 13 13

Functional Language Features: Iterators and Closures

Capturing environments, Fn traits, lazy iterator chains — the functional half of Rust.

11 questions 2 topics Senior

Closures

What is a closure, and how does it differ from a fn?

A closure is an anonymous function that can capture its environment — variables from the enclosing scope:

let x = 4;
let add = |y| y + x;     // captures x by reference
let add = |y: i32| -> i32 { y + x };  // explicit types allowed

fn can't capture — fn add(y) { y + x } won't compile. Type inference usually works (types lock on first use). Under the hood each closure is a unique anonymous struct holding captures — which is why closures are zero-cost and monomorphized like generics.

Explain Fn, FnMut, FnOnce — the three closure traits.

They describe how the closure treats captured environment:

- FnOnce — can be called once; may consume captures. Every closure implements it.

- FnMut — mutates captures; callable many times.

- Fn — only borrows captures; callable many times, safe to call concurrently.

Hierarchy: Fn ⊂ FnMut ⊂ FnOnce — anything Fn is also FnMut and FnOnce. APIs take the weakest bound they need: unwrap_or_else<F: FnOnce() -> T>, sort_by_key<F: FnMut(&T)->K>. The compiler picks the strongest impl the closure qualifies for.

How do closures decide between borrowing and moving captures — and what does move do?

The compiler infers the minimum needed: read-only capture → &, mutation → &mut, consumption → by value.

let list = vec![1, 2, 3];
let f = move || println!("{list:?}");   // takes ownership of list
// list unusable here now

move forces ownership transfer into the closure — required when the closure outlives its environment: thread::spawn(move || ...) (the thread may run after the function returns), returning closures, async blocks. Interview hook: move is about lifetime safety, not performance.

Show where the book uses closures in practice — unwrap_or_else and the t-shirt inventory.
let color = user_pref.unwrap_or_else(|| most_stocked_color(&inventory));

unwrap_or_else takes FnOnce() -> T — the fallback is a closure so it runs only when needed (laziness: unwrap_or(expensive()) would always run). The shirt example also shows a closure calling a regular function — closures are just values that happen to be callable.

Second pattern: sort_by_key(|shirt| shirt.price) — FnMut sorting keys where a fn can't express the captured context.

The sort_by_key borrow error — what goes wrong capturing &mut in a closure called repeatedly?

The book's gotcha: sort_by_key(|r| { ops.push(1); r.width }) — the closure mutates ops (Vec), so it needs FnMut. But sort_by_key calls the closure with &T and the closure's own &mut borrow of ops conflicts... the actual error: the closure tries to hold a &mut Vec while sort_by_key needs repeated calls — the closure must be FnMut, which is fine, unless it also captures something immutably borrowed elsewhere.

The fix pattern: precompute what's needed (let len = ops.len();) and capture the small Copy value instead of the whole Vec. Lesson: capture narrowly — grab fields/values, not the owner.

Iterators

Define the Iterator trait and its core contract.
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    /* ~70 methods with defaults: map, filter, fold... */
}

One required method (next returning Option) plus a huge default surface built on it. Iterators are lazy — map/filter return new iterators that do nothing until something calls next (via collect, sum, a for loop). type Item is an associated type — ch 20 revisits why.

iter() vs iter_mut() vs into_iter() — pick the right one.

- .iter() → yields &T — borrow elements, collection stays usable

- .iter_mut() → yields &mut T — mutate in place

- .into_iter() → yields T — consumes the collection, moves elements out

for x in v.iter() { ... }        // v still alive
for x in v.into_iter() { ... }   // v consumed
for x in v { ... }               // sugar: calls into_iter()

for loops call into_iter implicitly — for x in &v is the borrow form. Choosing wrong is a common borrow-check fight.

Consuming adaptors vs iterator adaptors — what's the distinction?

Adaptors (lazy): map, filter, zip, take, skip, enumerate — transform one iterator into another, do zero work until consumed. Consumers (eager): collect, sum, count, fold, for_each, last, find, any, all — drain the iterator into a result.

let evens: Vec<_> = (1..=10).filter(|x| x % 2 == 0).collect();
let total: i32 = nums.iter().map(|x| x * 2).sum();

Mistake people make: iter.map(f); alone — compiler even warns "iterators are lazy and do nothing unless consumed."

Why does collect sometimes need turbofish or a type annotation?

collect() can produce many collection types — Vec, HashSet, String, HashMap, even Result<Vec,E>:

let v: Vec<i32> = iter.collect();        // annotation
let v = iter.collect::<Vec<i32>>();      // turbofish
let words: HashSet<_> = text.split(' ').collect();

Rust infers it only if something else pins the type (return type, later use). When ambiguous → "type annotations needed." The collect::<Vec<_>> turbofish is the explicit form. Also the gem: iter.collect::<Result<Vec<_>,_>>()? — collects fallible items and short-circuits on first Err.

How can Rust claim iterators are zero-cost? Be specific.

Two mechanisms. Monomorphization: the concrete chain is fully type-known and inlined — x.iter().map(f).filter(g).collect() compiles down to the same tight loop as hand-written indexing. No runtime machinery: no boxing, no virtual dispatch, allocation only where the consumer asks for it.

The book cites that Rust's iterator code can beat equivalent manual loops — the optimizer sees the whole chain and elides bounds checks. That's why for x in iter isn't a convenience tax like in some languages; it's the fast path.

When should you NOT use iterator chains?

Honest cases: (1) Side-effect-heavy loops with early exits and complex flow — a for reads clearer than try_for_each gymnastics. (2) Error propagation in chains gets awkward (filter_map + collect::<Result> helps but has limits). (3) Very hot paths where you need unsafe or precise memory control — rare.

The book's own refactor stays tasteful: search becomes a filter+collect, but argument parsing keeps some imperative glue. Chains for transformations, loops for procedures.