| name | rust-iterators-closures |
| description | > Use when this capability is needed. |
Iterators & Closures
Rust's closures and iterators are zero-cost abstractions: the compiler lowers iterator
chains to the same machine code as hand-written loops, with no heap allocation and no
virtual dispatch. The Book devotes Chapter 13 to them precisely because mastering these
two features is the primary path from working Rust to idiomatic Rust.
The three rules to internalize before writing any loop:
- Iterators are lazy. Creating an adapter chain does nothing; you must attach a
consuming adaptor (
collect, sum, fold, for_each, …) to drive it.
- Closures capture by inference. The compiler picks immutable borrow → mutable borrow
→ move in that order;
move overrides the choice and forces ownership transfer.
Fn/FnMut/FnOnce form a hierarchy. Every closure implements at least FnOnce.
FnMut additionally allows mutation of captured values. Fn additionally guarantees
no mutation and no move-out — safe for concurrent calls.
When to Use
Invoke this skill proactively when any of the following appear:
- A C-style
for i in 0..v.len() loop that does not require the index
- An E0525 error ("expected a closure that implements
Fn/FnMut")
.unwrap() or .expect() inside a closure passed to .map() or .filter()
- An iterator adapter chain with no consuming terminal (compiler: "unused Map/Filter that must be used")
- A
move || closure that clones a large struct just before moving it, when Arc would suffice
- Any
FnOnce/FnMut/Fn bound mismatch in a method accepting a closure
Core Idioms
✅ Replace index loops with iterator adapters
let mut squares = Vec::new();
for i in 0..v.len() {
squares.push(v[i] * v[i]);
}
let squares: Vec<_> = v.iter().map(|x| x * x).collect();
✅ Collect a fallible iterator into Result<Vec<_>, E>
let parsed: Vec<u32> = strings.iter().map(|s| s.parse::<u32>().unwrap_or(0)).collect();
let parsed: Result<Vec<u32>, _> = strings.iter().map(|s| s.parse()).collect();
The turbofish is load-bearing: collect is generic over the output container; without it
the compiler cannot resolve which FromIterator impl to use.
✅ Use move closures for spawned tasks and 'static bounds
let list = vec![1, 2, 3];
thread::spawn(|| println!("{list:?}"));
thread::spawn(move || println!("{list:?}"));
Book (ch13-01): "If you want to force the closure to take ownership of the values it uses
in the environment even though the body of the closure doesn't strictly need ownership,
you can use the move keyword before the parameter list. This technique is mostly useful
when passing a closure to a new thread to move the data so that it's owned by the new
thread."
✅ Borrow, don't clone, inside closures
let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
let names: Vec<&str> = users.iter().map(|u| u.name.as_str()).collect();
let names: Vec<String> = users.into_iter().map(|u| u.name).collect();
✅ A lazy iterator must be consumed
v.iter().map(|x| x * 2);
let doubled: Vec<_> = v.iter().map(|x| x * 2).collect();
v.iter().map(|x| x * 2).for_each(|x| println!("{x}"));
✅ Choose iter / iter_mut / into_iter deliberately
| Method | Yields | Collection after |
|---|
v.iter() | &T — immutable refs | still valid |
v.iter_mut() | &mut T — mutable refs | still valid |
v.into_iter() | T — owned values | consumed / moved |
(Book ch13-02) into_iter takes ownership and yields owned values; iter_mut yields
mutable references.
Forbidden Patterns
Forbidden 1 — C-style index loop where an iterator adapter fits
for i in 0..items.len() {
process(items[i]);
}
for item in &items {
process(item);
}
for (i, item) in items.iter().enumerate() {
process_with_index(i, item);
}
Why (Book ch13-02): iterator adapters are lazy — the compiler can optimize chains to
the same code as a manual loop, with none of the off-by-one risk. Use enumerate when
the position is semantically required.
grep -rn 'for [a-z_]* in 0\.\.' src/
Forbidden 2 — Unused lazy iterator (map/filter with no terminal)
records.iter().map(|r| transform(r));
let out: Vec<_> = records.iter().map(|r| transform(r)).collect();
Why (Book ch13-02): iterators are lazy — they have no effect until you call methods
that consume the iterator. The compiler warning is the signal; treat it as an error.
grep -rn '\.map(|' src/ | grep -v '\.collect\|\.sum\|\.fold\|\.for_each\|\.find\|\.any\|\.all\|\.count\|\.max\|\.min\|\.last\|\.next\|\.position\|//\|#\[' || true
Forbidden 3 — Collect into Vec then immediately iterate again
let filtered: Vec<_> = records.iter().filter(|r| r.active).collect();
for r in &filtered {
notify(r);
}
for r in records.iter().filter(|r| r.active) {
notify(r);
}
let ids: Vec<_> = records.iter().filter(|r| r.active).map(|r| r.id).collect();
Why: the intermediate Vec is pure overhead. Iterator chains are lazy and the compiler
can fuse adjacent adapters. Allocate only when you actually need a concrete collection.
grep -rn '\.collect()' src/ -A2 | grep 'for '
Forbidden 4 — .clone() inside .map() to dodge a borrow
let labels: Vec<String> = items.iter().map(|i| i.label.clone()).collect();
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
let labels: Vec<String> = items.into_iter().map(|i| i.label).collect();
Why (Book ch13-01): closures capture by immutable borrow by default. A .clone()
that exists only because into_iter() was forgotten pays an allocation tax on every
element. Reach for .clone() only when the collection must survive and an owned copy is
genuinely needed by the downstream type.
grep -rn '\.map(|.*\.clone()' src/
Forbidden 5 — unwrap() / expect() inside .map() or .filter()
let counts: Vec<u32> = raw.iter().map(|s| s.parse::<u32>().unwrap()).collect();
let counts: Result<Vec<u32>, _> = raw.iter().map(|s| s.parse::<u32>()).collect();
let counts: Vec<u32> = raw.iter().map(|s| s.parse().unwrap_or(0)).collect();
Why (Book ch13-01, ch13-02 + rust-error-handling skill): panics inside iterator
closures propagate as unwind at an unpredictable point, bypassing structured error
handling. collect::<Result<Vec<_>, _>>() short-circuits on the first Err and returns
it to the caller — the correct mechanism for fallible iteration.
grep -rn -E '\.map\(\|.*\.unwrap\(\)' src/ | grep -v '//'
grep -rn -E '\.map\(\|.*\.expect\(' src/ | grep -v '//'
grep -rn -E '\.filter\(\|.*\.unwrap\(\)' src/ | grep -v '//'
grep -rn -E '\.filter\(\|.*\.expect\(' src/ | grep -v '//'
Forbidden 6 — move closure capturing a large struct by value unnecessarily
let cfg = Config::load();
for id in ids {
let cfg = cfg.clone();
tokio::spawn(async move { handle(id, cfg).await });
}
let cfg = Arc::new(Config::load());
for id in ids {
let cfg = Arc::clone(&cfg);
tokio::spawn(async move { handle(id, cfg).await });
}
Why (Book ch15-04, ch16-03): move transfers ownership of every captured variable.
For large or non-Copy data shared across tasks, Arc (atomic reference count) is the
standard mechanism — clone the Arc, not the payload. Reserve move for data that is
genuinely single-owner per closure invocation. See also the rust-smart-pointers skill.
grep -rn 'spawn(async move' src/ -B5 | grep -v 'Arc::\|Rc::\|//'
Forbidden 7 — Using FnOnce bound where the closure is called repeatedly
fn apply_all<F: FnOnce(u32) -> u32>(items: &[u32], f: F) -> Vec<u32> {
items.iter().map(|x| f(*x)).collect()
}
fn apply_all<F: FnMut(u32) -> u32>(items: &[u32], mut f: F) -> Vec<u32> {
items.iter().map(|x| f(*x)).collect()
}
fn apply_all<F: Fn(u32) -> u32>(items: &[u32], f: F) -> Vec<u32> {
items.iter().map(|x| f(*x)).collect()
}
Why (Book ch13-01): the three traits form a hierarchy — Fn ⊆ FnMut ⊆ FnOnce.
FnOnce promises the closure is called at most once (it may move values out of itself).
Use the loosest bound that satisfies the contract:
- Call once, may move out →
FnOnce
- Call many times, may mutate →
FnMut
- Call many times concurrently, no mutation →
Fn
Using a tighter bound than necessary rejects valid callers; using a looser bound than
necessary signals wrong intent and may compile but behave incorrectly.
grep -rn 'FnOnce' src/ | grep -v '// \|#\[cfg(test\|test::'
Verification Hooks
Run these detectors together before committing iterator/closure changes:
grep -rn 'for [a-z_]* in 0\.\.' src/
grep -rn '\.map(|' src/ | grep -v '\.collect\|\.sum\|\.fold\|\.for_each\|\.find\|\.any\|\.all\|\.count\|\.max\|\.min\|\.last\|\.next\|\.position\|//\|#\['
grep -rn '\.collect()' src/ -A2 | grep 'for '
grep -rn '\.map(|.*\.clone()' src/
grep -rn -E '\.map\(\|.*\.unwrap\(\)' src/ | grep -v '//'
grep -rn -E '\.map\(\|.*\.expect\(' src/ | grep -v '//'
grep -rn -E '\.filter\(\|.*\.unwrap\(\)' src/ | grep -v '//'
grep -rn -E '\.filter\(\|.*\.expect\(' src/ | grep -v '//'
grep -rn 'spawn(async move' src/ -B5 | grep -v 'Arc::\|Rc::\|//'
grep -rn 'FnOnce' src/ | grep -v '// \|#\[cfg(test\|test::'
Book References
Related Skills
- rust-ownership-borrowing — borrow checker mechanics that govern what closures can capture
- rust-error-handling —
AppError, ?, FromServerFnError; pairs with the
collect::<Result<_,_>>() pattern in Forbidden 5
- rust-collections —
Vec, HashMap, BTreeMap construction patterns; collect()
target types
Source: adelabdelgawad/rust-fullstack-agents — distributed by TomeVault.