The debugging bottleneck
You are building a data pipeline. Values flow from a file, through a filter, into a transformation, and finally into a database. Halfway through, something breaks. You need to see exactly what values are passing through a specific step without rewriting the whole chain. Or maybe you need to track a running total while filtering items. Rust's iterator methods inspect and scan solve these exact problems.
How inspect and scan actually work
Think of an iterator as a conveyor belt in a factory. inspect is a security camera mounted above the belt. It watches every item pass by, logs a timestamp, and lets the item continue untouched. The camera does not change the product. It only observes.
scan is a worker standing next to the belt with a clipboard. They look at each item, update their running tally, and hand a new value down the line. The worker remembers what happened in the previous step. This mutable state lives inside the iterator itself, not in your main function.
Both methods rely on closures. The closure you pass to inspect receives a reference to each item and returns nothing. The closure you pass to scan receives a mutable reference to your state and a reference to the current item. It must return an Option containing the transformed value.
Keep the camera passive. Let the belt keep moving.
Minimal examples
Here is the baseline syntax for both methods. The examples show the exact closure signatures and how the iterator chain consumes them.
/// Demonstrates inspect for observation and scan for stateful transformation.
fn main() {
let numbers = vec![1, 2, 3, 4];
// inspect takes a reference to each item and returns nothing.
// It is strictly for side effects like logging or debugging.
// The iterator forwards the original item unchanged.
numbers.iter()
.inspect(|n| println!("Watching: {}", n))
.count(); // count forces the iterator to run
// scan takes an initial state and a closure.
// The closure receives &mut state and &item.
// It must return Option<Output> to control the pipeline.
let running_sums: Vec<i32> = numbers.iter()
.scan(0, |state, &n| {
*state += n; // update the persistent state
Some(*state) // wrap in Some to pass the value downstream
})
.collect();
println!("Running sums: {:?}", running_sums);
}
The inspect call prints each number but leaves the iterator unchanged. The scan call builds a new vector where each element represents the cumulative total up to that point. Notice how scan wraps its result in Some. That wrapper is the control mechanism for the entire pipeline.
Walking through the execution
Iterators in Rust are lazy. Nothing happens until you call a consuming method like collect, count, or for_each. When consumption begins, the iterator pulls one item at a time and passes it through each method in the chain.
inspect receives the item, runs your closure, and immediately forwards the exact same item to the next method. It adds zero transformation overhead. The closure signature is FnMut(&T). It cannot mutate the item, and it cannot change the stream. The method exists purely to let you peek inside the black box.
scan works differently. It allocates a small piece of internal storage for your initial state. On the first call, it hands that state to your closure along with the first item. Your closure mutates the state, computes a result, and returns Some(value). The iterator stores that state for the next call. On the second item, the closure receives the updated state. This cycle repeats until the source iterator runs dry.
The Option return type is intentional. If your closure returns None, the iterator stops immediately. It does not pull the next item. It does not call the next method in the chain. The pipeline short circuits. This gives you a built in circuit breaker without breaking the functional style.
Treat the Option return as a circuit breaker. Pull it when the state goes bad.
The closure trait contract
Both methods require FnMut closures, not Fn. This distinction matters when you chain multiple methods. inspect mutates the closure environment implicitly because it may be called multiple times. scan mutates the closure environment explicitly because it carries state across calls.
If you try to pass a closure that captures variables by value without mut, the compiler rejects you with E0596 (cannot borrow as mutable). The iterator needs to call your closure repeatedly. It cannot clone the closure on every step. It mutates the closure in place. Declare your captured variables as mut if you intend to modify them inside the closure.
This trait requirement also explains why scan cannot use Fn. Fn closures promise not to change their captured environment. scan breaks that promise by design. The state must change, or the method is useless.
A realistic pipeline
Debugging and running totals are straightforward. Real code usually involves filtering, early termination, and external state tracking. Consider a transaction processor that calculates running balances and rejects any sequence that dips below zero.
/// Processes a stream of transactions and tracks the running balance.
/// Stops processing immediately if the balance goes negative.
fn process_transactions(transactions: Vec<i64>) -> Vec<i64> {
transactions.into_iter()
// scan maintains the balance across iterations
.scan(0, |balance, amount| {
*balance += amount; // update the running total
if *balance < 0 {
// return None to halt the iterator early
None
} else {
// return Some to continue the chain with the new balance
Some(*balance)
}
})
// collect gathers the intermediate balances into a vector
.collect()
}
fn main() {
let txns = vec![100, -50, 200, -300, 50];
let balances = process_transactions(txns);
println!("Balances before failure: {:?}", balances);
}
The scan closure mutates balance across calls. When the balance drops below zero, it returns None. The iterator drops the remaining items and collect finishes with whatever values made it through. This pattern replaces manual for loops with early break statements. The logic stays declarative. The state stays contained.
Common pitfalls and compiler errors
The closure signatures for these methods are strict. Rust will not guess your intent when the types do not align.
The most frequent mistake with scan is forgetting the Option wrapper. If you return a plain i32 instead of Option<i32>, the compiler rejects you with E0308 (mismatched types). The method signature demands Option<U>. Wrap your value in Some to keep the stream alive. Return None to stop it.
Another common error involves the state reference. scan passes &mut State to your closure. If you try to pass a closure that expects &State or State by value, you get a trait bound mismatch. The compiler expects FnMut(&mut S, &I) -> Option<U>. Make sure your closure parameters match that exact shape.
Developers sometimes overload inspect with heavy computation. inspect is designed for cheap side effects. If you perform file I/O or network calls inside it, you break the lazy evaluation contract. The iterator will stall, and your code becomes harder to reason about. Use map or for_each for heavy work. Reserve inspect for logging and debugging.
Match the closure signature exactly. The compiler will not guess your intent.
Convention asides
The Rust community treats inspect as a debugging tool. You will rarely see it in production code. If you find yourself using it to mutate external state, you are fighting the iterator model. Refactor to for_each or a standard loop.
When writing scan closures, keep the state type as small as possible. Large structs passed by mutable reference add indirection overhead. If you only need a counter or a running sum, stick to primitive types. The compiler optimizes small state beautifully.
Always write Rc::clone(&data) instead of data.clone() when working with reference counted types. The explicit form signals to readers that you are bumping a counter, not performing a deep copy. The same clarity applies to iterator chains. Name your intermediate variables when the chain exceeds three methods. Readability beats cleverness.
Choosing the right iterator method
Rust provides several methods for transforming and observing data. Picking the wrong one creates unnecessary boilerplate and fights the borrow checker.
Use inspect when you need to log or debug values without altering the stream. Keep it side effect only. Do not mutate external variables.
Use scan when you need to carry mutable state across iterations and produce a transformed output. Return Some to continue. Return None to halt.
Use fold when you only care about the final accumulated value, not the intermediate steps. fold consumes the iterator and returns a single result. It is faster than scan followed by collect because it skips the intermediate allocations.
Use map when each item transforms independently without remembering the past. map takes &T or T and returns a new value. It cannot hold state between calls.
Pick the tool that matches your data flow. Force a square peg into a round hole and the borrow checker will make you pay.