fold and reduce
When you have a collection of values and want to boil them down to a single result — a sum, a maximum, a concatenated sentence, a running average — Rust gives you two closely related iterator methods: fold and reduce. Both walk an iterator one item at a time, combining each item with an accumulated value using a closure you supply. They are among the most general tools in Rust’s iterator toolkit, because almost any “combine everything into one thing” operation can be expressed with a fold. Learning them well changes how you write loops in Rust: instead of manually mutating a variable inside a for loop, you reach for a single, declarative call.
Overview: How fold and reduce work
fold is built around three ingredients: an iterator, a starting value (the initial accumulator), and a closure that describes how to combine the current accumulator with the next item. On every step, fold calls your closure with (accumulator, item) and the closure returns the new accumulator, which becomes the input to the next call. When the iterator is exhausted, fold returns the final accumulator value. Nothing is mutated behind your back — the accumulator is moved into the closure and a (possibly new) value is moved back out on every single call.
Trace it by hand for [1, 2, 3].iter().fold(0, |acc, x| acc + x): the accumulator starts at 0. Step 1 combines 0 with 1, producing 1. Step 2 combines 1 with 2, producing 3. Step 3 combines 3 with 3, producing 6. The iterator is now empty, so fold returns 6. That is the entire mental model — no magic, just repeated application of your closure, threading one value through every element.
reduce is fold‘s sibling with one difference: instead of asking you for an initial accumulator, it uses the first element of the iterator itself as the seed, then folds over the rest. Because the iterator might be empty (in which case there is no “first element” to seed with), reduce returns Option<T> rather than T directly — None for an empty iterator, Some(value) otherwise. Use fold when you know the “zero value” for your operation (like 0 for sums or an empty String for concatenation) or when your accumulator has a different type than the items (say, building a Vec<String> from an iterator of numbers). Use reduce when the accumulator and item are the same type and there genuinely is no sensible default seed — like finding the maximum of a list of integers, where there’s no universally correct “empty” value to start from.
Both methods are eager: calling fold or reduce immediately consumes the entire iterator to produce a result (unlike lazy adapters such as map or filter, which do nothing until something drives them). This means calling either on an infinite iterator (like an unbounded 0.. range) without a prior take will loop forever.
Syntax
iterator.fold(initial_value, |accumulator, item| {
// combine accumulator and item, return the new accumulator
});
iterator.reduce(|accumulator, item| {
// combine accumulator and item, return the new accumulator
}); // returns Option<Item>
| Part | Meaning |
|---|---|
initial_value |
The starting accumulator for fold; its type determines the return type of fold. |
accumulator |
The running value, moved into the closure and moved back out each call. |
item |
The next element produced by the iterator (by value, or by reference if you’re iterating with .iter()). |
| closure return | Must evaluate to the new accumulator — the last expression in the closure body, with no trailing semicolon. |
reduce return |
Option<Item> — None if the iterator was empty, Some(result) otherwise. |
Examples
Example 1: Summing with fold
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum_of_squares: i32 = numbers.iter().fold(0, |acc, &x| acc + x * x);
println!("Sum of squares: {}", sum_of_squares);
}
Output:
Sum of squares: 55
The initial accumulator is 0. Iterating with .iter() yields &i32 references, so the closure pattern &x dereferences each one into a plain i32. Each step adds that item’s square to the running total: 1 + 4 + 9 + 16 + 25 = 55.
Example 2: Building a String with fold
fn main() {
let words = vec!["Rust", "is", "fast"];
let sentence = words.iter().fold(String::new(), |mut acc, &word| {
if !acc.is_empty() {
acc.push(' ');
}
acc.push_str(word);
acc
});
println!("{}", sentence);
}
Output:
Rust is fast
Here the accumulator type (String) is completely different from the item type (&&str, since words.iter() yields references into a Vec<&str>). This is exactly the case where fold shines and reduce cannot help, because reduce requires the accumulator and item to share a type. Each call takes ownership of acc (marked mut so we can push into it), mutates it, and returns it as the closure’s final expression so it becomes the next accumulator.
Example 3: Finding a maximum with reduce
fn main() {
let numbers = vec![3, 7, 2, 9, 4];
let max = numbers.iter().copied().reduce(|a, b| if a > b { a } else { b });
match max {
Some(m) => println!("Max: {}", m),
None => println!("The collection was empty"),
}
}
Output:
Max: 9
.copied() turns the &i32 references from .iter() into plain i32 values (cheap, since i32 is Copy), so both a and b in the closure are i32. reduce seeds the accumulator with the first element (3) and folds the rest against it, always keeping the larger of the two. Because the result is Option<i32>, we match on it instead of assuming success — this is exactly why reduce exists as a distinct, safer-by-default method rather than always requiring you to guess a seed.
Example 4: A realistic accumulator — running average
fn main() {
let scores = vec![85, 92, 78, 90, 88];
let (count, total) = scores.iter().fold((0, 0), |(count, total), &score| {
(count + 1, total + score)
});
let average = total as f64 / count as f64;
println!("Average: {:.2}", average);
}
Output:
Average: 86.60
This is where fold becomes genuinely powerful in production code: the accumulator is a (i32, i32) tuple tracking both a running count and a running total in a single pass, something a naive .sum() call couldn’t do alone. Each step destructures the accumulator tuple, increments the count, and adds the score to the total, returning a new tuple. After the fold, ordinary arithmetic converts the pair into an average.
How it works step by step
For numbers.iter().fold(0, |acc, x| acc + x) on [1, 2, 3], the compiler-generated loop conceptually does this:
| Step | Accumulator in | Item | Closure call | Accumulator out |
|---|---|---|---|---|
| 1 | 0 | 1 | 0 + 1 | 1 |
| 2 | 1 | 2 | 1 + 2 | 3 |
| 3 | 3 | 3 | 3 + 3 | 6 |
| done | 6 | — | iterator exhausted | return 6 |
Internally, fold‘s implementation is little more than a while let Some(item) = iter.next() loop that reassigns a local accumulator variable to the closure’s result on every iteration — there is no hidden allocation or special runtime support. reduce does the same thing, except its very first step calls iter.next() once to obtain the seed; if that first call returns None (empty iterator), reduce short-circuits and returns None without ever calling your closure.
Common Mistakes
Mistake 1: Treating reduce’s result as the item type instead of Option
It’s easy to forget that reduce can fail to produce a value (empty iterator), so its return type is always wrapped in Option.
fn main() {
let numbers = vec![1, 2, 3];
let sum: i32 = numbers.iter().copied().reduce(|a, b| a + b);
println!("{}", sum);
}
This fails to compile with a type mismatch, roughly:
error[E0308]: mismatched types
|
| let sum: i32 = numbers.iter().copied().reduce(|a, b| a + b);
| --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Option<i32>`
The fix is to unwrap the Option deliberately, deciding what should happen on an empty iterator:
fn main() {
let numbers = vec![1, 2, 3];
let sum: i32 = numbers.iter().copied().reduce(|a, b| a + b).unwrap_or(0);
println!("{}", sum);
}
Output:
6
Mistake 2: Forgetting to return the accumulator from the fold closure
The closure’s last expression must be the new accumulator, with no trailing semicolon. Adding a semicolon turns it into a statement that evaluates to () instead.
fn main() {
let numbers = vec![1, 2, 3];
let doubled = numbers.iter().fold(Vec::new(), |mut acc, &x| {
acc.push(x * 2);
});
println!("{:?}", doubled);
}
The closure body’s only statement ends with a semicolon, so the block’s value is (), but fold expects the closure to return the accumulator type (Vec<i32>, inferred from Vec::new() and the pushed elements). The compiler rejects this:
error[E0308]: mismatched types
|
| let doubled = numbers.iter().fold(Vec::new(), |mut acc, &x| {
| ^^^^^^^^^^^^^ expected `Vec<i32>`, found `()`
Drop the semicolon after the last line (or add acc as a final expression) so the block evaluates to the accumulator:
fn main() {
let numbers = vec![1, 2, 3];
let doubled = numbers.iter().fold(Vec::new(), |mut acc, &x| {
acc.push(x * 2);
acc
});
println!("{:?}", doubled);
}
Output:
[2, 4, 6]
Mistake 3: Calling unwrap on reduce without considering an empty collection
Because reduce returns None for an empty iterator, blindly calling .unwrap() on the result compiles fine but is a runtime panic waiting to happen the moment the input collection is empty:
fn main() {
let numbers: Vec<i32> = Vec::new();
let max = numbers.iter().copied().reduce(|a, b| if a > b { a } else { b }).unwrap();
println!("Max: {}", max);
}
Output:
thread 'main' panicked at ...: called `Option::unwrap()` on a `None` value
This program type-checks and compiles without any error, but running it with an empty numbers vector panics before the println! ever runs, with a message like thread 'main' panicked at ... called \`Option::unwrap()\` on a \`None\` value. Handle the empty case explicitly instead of assuming data will always be present:
fn main() {
let numbers: Vec<i32> = Vec::new();
let max = numbers.iter().copied().reduce(|a, b| if a > b { a } else { b });
match max {
Some(m) => println!("Max: {}", m),
None => println!("The collection was empty"),
}
}
Output:
The collection was empty
Best Practices
- Reach for
foldwhen your accumulator’s type differs from the iterator’s item type (building aStringfrom words, aVecfrom numbers, a struct from records). - Reach for
reduceonly when accumulator and item share a type and there’s no natural “zero” value to seed with, like a maximum or a custom merge operation. - Prefer the built-in specialized methods (
.sum(),.product(),.max(),.min(),.count()) over a hand-writtenfold/reducewhen they say exactly what you mean — they’re clearer to read and just as fast. - Always end a
foldclosure’s block with the accumulator as the final expression (no semicolon) — a stray semicolon is one of the most common fold compile errors. - Handle
reduce‘sOption<T>result explicitly withmatch,if let, or a deliberate.unwrap_or(...)rather than reaching for.unwrap()by default. - Avoid
fold/reduceon iterators that might be unbounded unless you’ve first bounded them with.take(n)— both methods run to exhaustion before returning anything. - When the accumulator is a tuple or small struct, name the fields clearly in the destructuring pattern (
|(count, total), item|) so the closure reads like documentation.
Practice Exercises
- Given
let words = vec!["the", "quick", "brown", "fox"];, usefoldto compute the total number of characters across all words as a singleusize. Expected output:17. - Given
let numbers = vec![4, 1, 7, 3, 9, 2];, usereduceto find the smallest value, printing it with amatchthat also handles the empty-vector case. Expected output:1. - Given
let numbers = vec![1, 2, 3, 4];, usefoldwith a tuple accumulator(i32, i32)to simultaneously compute the sum and the product of all elements, then print both. Expected output:Sum: 10, Product: 24.
Summary
foldcombines every item of an iterator into a single accumulator, starting from an initial value you supply and returning the accumulator’s final type directly.reduceisfoldwithout an explicit seed — it uses the iterator’s first element as the starting accumulator, so it returnsOption<T>to account for an empty iterator.- Use
foldwhen the accumulator type differs from the item type; usereduceonly when they’re the same type and there’s no sensible zero value. - Both are eager: they consume the whole iterator immediately and will hang on an unbounded iterator without a prior
.take(n). - The fold closure must return the new accumulator as its final expression — a trailing semicolon that turns the block into
()is a very common compile error. - Always handle
reduce‘sOptionresult explicitly instead of defaulting to.unwrap(), which panics on an empty collection.
