Iterators Recap and Deepening with Closures
A thorough recap of Rust iterators and how closures power iterator adapters, with a deep look at lazy evaluation and zero-cost abstractions.
An iterator in Rust is any type that implements the Iterator trait, which requires a single method: fn next(&mut self) -> Option<Self::Item>. Calling next() returns Some(item) until there are no more items, at which point it returns None and signals the end of the sequence. The for loop you use every day desugars directly into this next-driven machinery. Everything else—mapping, filtering, collecting—builds on that simple contract.
What makes iterators feel “functional” is that they pair naturally with closures. A closure is an anonymous function that can capture variables from its surrounding scope, and when you pass one to an iterator adapter like map or filter, you describe what should happen to each element without managing a loop counter, an index, or a mutable accumulator. This document recaps the core iterator concepts and then deepens them around closures: how iterator adapters consume closures, how laziness shapes the execution model, and why all of this abstraction compiles away to the same machine code you would write by hand.
Iterator Adapters and Closures
An iterator adapter is a method on an iterator that returns a new iterator. It does not consume the original data; instead it wraps the existing iterator and transforms the stream of items as they pass through. The transformation is defined by a closure you supply.
The most common adapters—map, filter, take, skip, enumerate, flat_map—all accept closures that implement one of the Fn traits. The trait bound chosen by each adapter tells you exactly what the closure is allowed to do with its environment.
let numbers = vec![1, 2, 3, 4];
let doubled: Vec<i32> = numbers
.iter()
.map(|x| x * 2)
.collect();
map takes a closure that receives a reference to each item (because iter() yields &i32). The closure |x| x * 2 is a Fn closure—it only reads its argument and returns a value, never mutating any captured state. The map call produces a new iterator, but no work happens yet. Only when collect() calls next() repeatedly does the multiplication execute.
When a closure needs to borrow something from the enclosing scope, Rust infers how the closure captures it. The same rules of ownership and borrowing apply, and the iterator chain must respect them.
let factor = 3;
let numbers = vec![1, 2, 3];
let scaled: Vec<i32> = numbers
.iter()
.map(|x| x * factor)
.collect();
The closure captures factor by immutable reference because that is all the arithmetic needs. The borrow lasts as long as the closure exists—until collect finishes—so factor cannot be mutated elsewhere during that time. This is the same borrow-checker discipline you already know, now applied to closures embedded in iterator chains.
Closures and the Fn Traits:
Iterators are designed to work with any callable that meets their trait bounds. map requires FnMut (which includes Fn), filter requires FnMut, and for_each requires FnOnce. A regular function pointer also satisfies these traits, so you can pass function names instead of closures when you don’t need to capture anything.
How Different Adapters Use Closures
Each adapter imposes a different interaction pattern between the closure and the items.
mapapplies the closure to every item and yields the result. The closure must beFnMutbecause the adapter calls it repeatedly. It can mutate captured state, like a counter, if it needs to.filterpasses each item to the closure and keeps only those for which the closure returnstrue. The closure must also beFnMut.filter_mapmergesfilterandmap: the closure returnsOption<T>, and onlySomevalues are yielded.flat_mapexpects a closure that returns something iterable (IntoIterator). The adapter flattens all yielded items into a single stream.take_whileandskip_whileuse a predicate closure to control how many items are pulled from the underlying iterator.
The ownership rules inside these closures often surprise newcomers. Because the closures are called one at a time, the borrows on captured variables can last across multiple calls. If the closure mutates a captured Vec or HashMap, that mutable borrow prevents any other access to that collection until the iterator is consumed or dropped.
Mutable borrows across iterator chains:
If you pass a closure that mutably borrows a collection, you cannot use that collection elsewhere while the iterator is alive. The compiler will reject reading or writing the same collection until the iterator is dropped. This is a frequent source of errors when building pipelines that update state alongside iteration.
Moving Values into Closures
Some closures need to take ownership of captured data, especially when the iterator is passed to another thread or when the captured value is consumed inside the closure. Adding the move keyword before the parameter list forces the closure to take ownership of everything it captures.
let name = String::from("task");
let tasks = vec!["a", "b", "c"];
let processed: Vec<String> = tasks
.iter()
.map(move |t| format!("{}-{}", name, t))
.collect();
Here name is moved into the closure. The move keyword is necessary because the closure outlives the scope where name was defined? Not exactly—it is necessary because the closure is used inside a method that expects a 'static bound in certain contexts, or because the compiler otherwise can't prove the capture will live long enough. But even in simple chains like this, move makes the ownership transfer explicit. Once name is moved, the original variable cannot be used again.
Using a consumed iterator after consumption:
Methods like sum, collect, count, and for_each consume the iterator by repeatedly calling next() until None. After that call, the iterator is no longer valid. Any attempt to call next() or another consuming adapter on the same value will result in a compiler error.
Real-World Usage: Data Transformation Pipelines
In production Rust code, iterator chains with closures replace many imperative loops. A function that reads a file, filters lines, and parses numbers might look like this:
fn sum_of_positive_integers(text: &str) -> i32 {
text.lines()
.filter_map(|line| line.trim().parse::<i32>().ok())
.filter(|&n| n > 0)
.sum()
}
Each closure communicates intent without indexing, temporary vectors, or error-prone loop variables. The filter_map closure tries to parse a line; if it fails, ok() turns the Result into Option, and filter_map skips the None. The filter closure keeps only positive numbers. Finally sum consumes the iterator and folds the values together.
Beginners can think of the whole chain as a description of the work, not a series of immediate actions. The execution only starts when sum demands the first value; at that point the pipeline pulls items from lines(), passes them through the closures, and accumulates the total. The closures themselves look like tiny helper functions but without the boilerplate of separate fn definitions.
Laziness and Zero-Cost Abstractions
Rust iterators are lazy by default. Calling map, filter, or any adapter method on an iterator does not immediately perform any computation. It merely constructs a new iterator struct that stores the original iterator and the closure, waiting for a consumer to start calling next().
This laziness has two major consequences. First, you can build up complex pipelines without paying for intermediate allocations. A chain of map and filter does not create a temporary Vec for each step. Second, if you forget to call a consuming method (like collect, sum, fold, for_each), the entire pipeline is silently dead code—the compiler will warn about unused values, but the logic never runs.
let numbers = vec![1, 2, 3];
numbers.iter().map(|x| x + 1); // nothing happens
The line above produces a warning: unused implementer of Iterator that must be used. Rust nudges you to consume the iterator because, without a consumer, the closure never executes.
How Laziness Enables Efficient Composition
Imagine you need the sum of the first five even numbers from a large list. With an eager approach, you might filter all even numbers into a new list, take the first five, and sum them, allocating a vector you don't need. With lazy iterators, you can write:
let sum: i32 = (1..)
.filter(|x| x % 2 == 0)
.take(5)
.sum();
This pipeline starts with an infinite range. filter only inspects numbers as take asks for them. take stops after five elements, at which point sum finishes. No intermediate collection is allocated, and the infinite range never materializes beyond what is needed. The closures run only on the data that survives the filtering and the take limit.
Zero-Cost Abstraction
“Zero-cost abstraction” means that the high-level iterator code you write compiles down to the same machine code as an equivalent hand-written loop. The compiler inlines the closures and fuses the iterator adapters, eliminating any overhead of function calls or extra structs.
Consider two functionally identical pieces of code:
// Version A: iterator chain
let sum: i32 = vec![1, 2, 3, 4, 5]
.iter()
.map(|x| x * 2)
.sum();
// Version B: hand-written loop
let mut sum = 0;
let items = vec![1, 2, 3, 4, 5];
for x in &items {
sum += x * 2;
}
With optimizations enabled, both produce equivalent assembly. The map closure is inlined; the iterator state machine becomes a simple loop; the temporary Map struct disappears. You get the clarity of the functional style without a runtime penalty.
Performance guaranteed by design:
Rust’s ownership system makes this possible. Because iterators and closures have no hidden allocation or reference counting, the compiler can see through them and apply aggressive optimizations. The resulting binary is as fast as carefully tuned imperative code.
Common Misconceptions About Laziness
A frequent mistake is assuming that an iterator adapter modifies the underlying collection. It never does. map does not change the original vector; it produces a new iterator that yields transformed values. If you need the transformed data in a new collection, you must call collect or a similar consumer.
Another subtle point is that lazy iterators can hold references to data. If the iterator is not consumed immediately and the data goes out of scope, you will get a borrow-checker error. The compiler prevents dangling references by ensuring the iterator’s lifetime does not exceed the referenced data’s lifetime. This same rule protects closures that capture references to local variables: the iterator chain must finish before the borrow expires.
Iterator lifetimes and captured references:
When a closure captures a reference to a local variable, the iterator that holds that closure cannot outlive the variable. For example, you cannot return an iterator chain from a function if the chain borrows a local string. Use collect to own the result, or restructure the code to pass owned data.
The Iterator Source Determines Borrowing
Rust provides three primary ways to get an iterator from a collection: iter(), iter_mut(), and into_iter(). The choice controls whether closures inside adapters see references or owned values.
iter()yields&Titems. Closures receive immutable references, ideal for read-only transformations.iter_mut()yields&mut T. Adapters can mutate items in place, but the mutable borrow rules apply.into_iter()consumes the collection and yields ownedT. Closures work with owned values, and the original collection is no longer available. Use this when the pipeline needs to move data out or transfer ownership.
Each variant produces a different iterator type, but the adapter methods work the same way—the closures simply operate on the item type the iterator provides.