Chapter 9: Closures and Iterators
Closures and iterators work together to help you express data processing as clear pipelines. Closures are anonymous functions that can capture values from their environment. Iterators produce items one by one and only do work when you ask for the next value. Combined, they let you write compact code that reads like a description of the task while staying efficient.
Closure capture modes
Rust classifies a closure by how it captures variables from the environment. The compiler chooses the least intrusive mode that still works for the closure body. The three traits are Fn (shared reference), FnMut (unique mutable reference), and FnOnce (by value). You can also force capture with the move keyword when you need ownership to transfer into the closure.
Shared capture with Fn
When a closure only reads from captured variables, the compiler uses Fn. This means the closure takes shared references to its environment and can be called many times in parallel contexts that accept Fn.
fn main() {
let threshold = 10;
let is_big = |x: &i32| *x > threshold; // captures by shared reference (Fn)
assert!(is_big(&12));
assert!(!is_big(&5));
}
Mutable capture with FnMut
If a closure needs to mutate captured state, it becomes FnMut. You must call it with mut and only one caller may hold it at a time while it mutates its environment.
fn main() {
let mut count = 0;
let mut bump = || { count += 1; count }; // captures by &mut (FnMut)
assert_eq!(bump(), 1);
assert_eq!(bump(), 2);
// count is now 2
}
FnMut, which is sufficient for most iterator adapters that change state such as scan or map when they mutate an external counter.
By-value capture with FnOnce and move
When a closure takes ownership of a captured value, it implements at least FnOnce. A FnOnce closure can be called only one time because captured ownership may be consumed. Use move to require by-value capture.
fn make_printer() -> impl FnOnce() {
let s = String::from("owned");
move || println!("{s}") // takes ownership of s (FnOnce)
}
fn main() {
let print_once = make_printer();
print_once();
}
Fn (and also FnMut and FnOnce). If it mutates, it implements FnMut (and FnOnce). If it moves out of captured variables, it implements only FnOnce.
Iterator adapters
The Iterator trait models a sequence that yields items on demand. Adapters create new iterators from existing ones without consuming all the data up front. You compose adapters to express a pipeline, then finish with a consumer such as collect, for, or fold.
Mapping and filtering
Use map to transform each item and filter to keep only items that satisfy a predicate. Both are lazy, so nothing happens until a consumer pulls values.
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let evens_squared = nums
.iter()
.copied()
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.collect::<Vec<i32>>();
assert_eq!(evens_squared, vec![4, 16]);
}
Taking, skipping, and enumerating
take limits the number of items, skip ignores a prefix, and enumerate pairs items with their index. These are useful for paging and positional logic.
fn main() {
let slice = (100..).skip(3).take(4).collect::<Vec<i32>>();
assert_eq!(slice, vec![103, 104, 105, 106]);
let labels = ["a", "b", "c"];
for (i, s) in labels.iter().enumerate() {
println!("{i}: {s}");
}
}
Folding and reducing
fold accumulates a result by visiting each item with a closure that updates state. It is the general form of many aggregations.
fn main() {
let product = [2, 3, 4].iter().copied().fold(1, |acc, n| acc * n);
assert_eq!(product, 24);
}
try_fold and try_for_each, which short-circuit on Err. These integrate cleanly with Result based workflows.
IntoIterator and FromIterator
IntoIterator describes types that can produce an iterator. The for loop uses this trait behind the scenes. FromIterator builds a collection from an iterator. Together they connect collections and pipelines.
IntoIterator for references and ownership
Collections often implement IntoIterator in three ways: by value (consumes and yields owned items), by shared reference (yields references), and by mutable reference (yields mutable references). Choose the form that suits your needs.
fn main() {
let v = vec![String::from("a"), String::from("b")];
for s in &v { // &Vec<String> - yields &String
println!("{s}");
}
for s in &mut v.clone() { // &mut Vec<String> - yields &mut String
s.push_str("!");
}
for s in v.clone() { // Vec<String> - yields String, consumes v
println!("{s}");
}
}
Collecting with FromIterator
collect uses FromIterator to build a target type. Use turbofish or an annotation to select the destination. Many standard collections implement FromIterator, including Vec, String, HashSet, and HashMap.
use std::collections::HashMap;
fn main() {
let pairs = vec![("a", 1), ("b", 2)];
let map = pairs.into_iter().collect::<HashMap<_, _>>();
assert_eq!(map.get("b"), Some(&2));
}
collect, guide it with a concrete type. For example, add : Vec<i32> to a binding or use turbofish on the method call.
Building custom iterators
You can define your own iterator by creating a stateful struct and implementing Iterator for it. The only required method is next, which returns Option<Item>. Each call to next advances the state.
Implementing Iterator for a counter
This example yields square numbers up to a limit. The struct holds the current value and the limit, and next updates the state before returning the next item.
struct Squares {
cur: u32,
max: u32,
}
impl Squares {
fn up_to(max: u32) -> Self {
Self { cur: 0, max }
}
}
impl Iterator for Squares {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.cur > self.max {
return None;
}
let n = self.cur;
self.cur += 1;
Some(n * n)
}
}
fn main() {
let out = Squares::up_to(5).skip(2).take(3).collect::<Vec<u32>>();
assert_eq!(out, vec![4, 9, 16]);
}
Providing IntoIterator for your type
To support for loops over your collection type, implement IntoIterator. Decide whether iteration should consume the collection or yield references.
struct Bag<T> {
items: Vec<T>,
// … other fields
}
impl<T> IntoIterator for Bag<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
}
}
IntoIterator for &Bag<T> and &mut Bag<T> to yield references without consuming the bag. Mirror how Vec<T> does it for a familiar user experience.
Lazy pipelines in real programs
Iterator chains are lazy. Nothing executes until a consumer pulls results. This helps with large inputs, streaming I/O, and early exit when you only need a few items. The following example scans a file for the first matching record and stops as soon as it finds one.
Processing text incrementally
The std::io::BufRead API exposes lines, which returns an iterator of Result<String, io::Error>. Combine adapters with find_map to stop at the first interesting item.
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn first_id(path: &str) -> io::Result<u64> {
let file = File::open(path)?;
let reader = BufReader::new(file);
reader
.lines()
.filter_map(|res| res.ok())
.find_map(|line| {
line.strip_prefix("id=")
.and_then(|s| s.parse::<u64>().ok())
})
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no id found"))
}
fn main() -> io::Result<()> {
println!("{}", first_id("data.txt")?);
Ok(())
}
Composing fallible steps with Iterator
When each step can fail, use filter_map, map_while, or try_fold. This keeps the pipeline linear and avoids nested loops and conditionals.
use std::num::ParseIntError;
fn sum_first_k(nums: &[&str], k: usize) -> Result<i64, ParseIntError> {
nums.iter()
.map(|s| s.parse::<i64>())
.take(k)
.try_fold(0_i64, |acc, n| n.map(|x| acc + x))
}
Vec values unless you truly need random access. Staying in iterator form lets Rust fuse adapters and minimize allocations.
When to choose a loop instead
Prefer an explicit for loop when the logic needs complex early returns, multiple mutation points, or cross-item state that does not map cleanly to adapters. Both styles compile to efficient code, so choose the one that reads best.