The pipeline that doesn't allocate
You have a list of raw strings from a CSV file. You need to parse them into numbers, drop the ones that are negative, and calculate the total. In Python, you might reach for a list comprehension. In JavaScript, you chain .map().filter().reduce(). Rust gives you the same power with a twist. The iterator chain doesn't allocate intermediate collections. You describe the pipeline, and Rust executes it item by item. This keeps memory usage flat and lets the compiler optimize the whole chain into a tight loop.
Describe the pipeline. Let the compiler build the loop.
Iterator adapters and lazy evaluation
An iterator is a sequence of values produced one at a time. Iterator adapters like map, filter, and fold take an iterator and return a new iterator. They don't run the logic immediately. They build a description of work. This is called lazy evaluation.
Think of an iterator as a conveyor belt moving items. map is a robot arm that grabs each item, modifies it, and puts the result back on the belt. filter is a quality check station that tosses items off the belt if they don't meet the spec. fold is a machine at the end that takes items and smashes them together into a single product.
Nothing happens until you ask for the result. You can stack fifty adapters, and Rust won't do a single operation until you call a consuming method like collect or fold. When you consume the iterator, the chain executes item by item. Each item flows through the adapters, gets transformed or dropped, and emerges at the end. No intermediate vectors are created.
Build the chain. Consume it later. Laziness is your friend.
Minimal example
/// Demonstrates basic iterator adapters: map, filter, and fold.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Map transforms each item. The closure receives a reference.
// We destructure the reference to get the value.
let squared: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
// Filter keeps items where the closure returns true.
// The closure receives a reference to the item.
let evens: Vec<i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
// Fold reduces the iterator to a single value.
// The accumulator starts at 0. Each step adds the next item.
let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
}
Annotate the result type. The compiler needs the target to pick the right methods.
How the chain executes
When you write numbers.iter().map(...).filter(...), you are building a stack of iterators. iter() returns an Iter over references. map wraps that in a Map iterator. filter wraps that in a Filter iterator. The chain is a nested structure of iterators, each holding the previous one and a closure.
When you call .collect(), the collect method calls next() on the Filter iterator. The Filter calls next() on the Map. The Map calls next() on the Iter. The Iter returns the first reference. The Map applies its closure and returns the result. The Filter applies its predicate. If the predicate is false, the Filter calls next() again immediately. If true, it returns the value to collect.
This happens one item at a time. The compiler sees the whole chain and often inlines everything into a single for loop at assembly level. The performance matches a hand-written loop, but the code is declarative and less error-prone.
Closures and captures
Closures are anonymous functions that can capture variables from their environment. The syntax is |args| body. Closures capture by reference by default. This means they borrow the variables they use.
/// Shows closure capture behavior.
fn main() {
let multiplier = 10;
// The closure captures multiplier by reference.
// It borrows multiplier for the duration of the iterator.
let result: Vec<i32> = vec![1, 2, 3]
.iter()
.map(|x| x * multiplier)
.collect();
// multiplier is still usable here because the borrow ended.
println!("Multiplier: {}", multiplier);
}
If you need the closure to own the captured variables, use the move keyword. This moves the values into the closure. Use move when the closure outlives the variables, or when you need to pass the closure to another thread.
/// Demonstrates move closure.
fn main() {
let data = vec![1, 2, 3];
// move forces the closure to take ownership of data.
// data cannot be used after this point.
let _process = std::thread::spawn(move || {
data.iter().sum::<i32>()
});
}
If you try to mutate a captured variable inside a closure without declaring it mutable, the compiler rejects you with E0596 (cannot borrow as mutable). Declare the variable as mut before capturing it.
Reach for move when the closure needs ownership. Otherwise, let the compiler borrow.
Realistic example
You are processing a list of configuration entries. Each entry is a string. You need to parse the string into a number, skip entries that fail to parse, keep only positive values, and sum the result.
/// Parses config entries, filters valid positive numbers, and sums them.
fn sum_config_values(entries: Vec<String>) -> i64 {
entries
.into_iter() // Move ownership of strings.
.filter_map(|s| {
// Parse the string. Return None on error to skip.
// filter_map is the convention for fallible transformations.
s.parse::<i64>().ok()
})
.filter(|&n| n > 0) // Keep only positive numbers.
.fold(0, |acc, n| acc + n) // Sum the results.
}
The filter_map adapter combines map and filter. It applies a closure that returns an Option. If the result is Some, the value passes through. If None, the item is dropped. This is more efficient than chaining map and filter because it avoids the intermediate check.
Convention is to use filter_map when parsing. It's the standard pattern for "try to transform, skip if fail".
Reach for filter_map when parsing. It's the community standard for skipping errors.
Pitfalls and compiler errors
Unconsumed iterators are a silent no-op. If you chain adapters but forget to call collect, fold, or another consuming method, the code compiles and does nothing. The compiler won't warn you. This is a logic bug. Always ensure the iterator is consumed.
// This compiles but does nothing.
// The iterator is created and immediately dropped.
let _ = vec![1, 2, 3].iter().map(|x| x * 2);
Type inference can fail if the compiler can't determine the output type. map returns an iterator, but the compiler needs to know what type to collect into. If you omit the type annotation, you might get E0277 (trait bound not satisfied) or E0283 (type annotations needed).
// E0283: type annotations needed.
// The compiler doesn't know what type to collect into.
let _ = vec![1, 2, 3].iter().map(|x| x * 2).collect();
// Fix: annotate the type.
let result: Vec<i32> = vec![1, 2, 3].iter().map(|x| x * 2).collect();
Closures must match the expected signature. If map expects a closure that returns a value, and your closure returns (), you get E0308 (mismatched types). Check the closure return type carefully.
// E0308: mismatched types.
// map expects a return value, but println returns ().
let _ = vec![1, 2, 3].iter().map(|x| println!("{}", x));
Call collect or fold. An unconsumed iterator is a silent no-op.
When to use map, filter, and fold
Use map when you need to transform every item in a collection into a new value. Use filter when you need to keep only items that satisfy a condition. Use fold when you need to reduce a collection to a single value by accumulating state. Use a for loop when the logic is too complex for a chain, or when you need to break early based on side effects. Use for_each when you want to perform a side effect on every item and don't need a return value.
Match the tool to the intent. The compiler sees through the abstraction.