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 fold when your accumulator’s type differs from the iterator’s item type (building a String from words, a Vec from numbers, a struct from records).
  • Reach for reduce only 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-written fold/reduce when they say exactly what you mean — they’re clearer to read and just as fast.
  • Always end a fold closure’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‘s Option<T> result explicitly with match, if let, or a deliberate .unwrap_or(...) rather than reaching for .unwrap() by default.
  • Avoid fold/reduce on 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"];, use fold to compute the total number of characters across all words as a single usize. Expected output: 17.
  • Given let numbers = vec![4, 1, 7, 3, 9, 2];, use reduce to find the smallest value, printing it with a match that also handles the empty-vector case. Expected output: 1.
  • Given let numbers = vec![1, 2, 3, 4];, use fold with 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

  • fold combines every item of an iterator into a single accumulator, starting from an initial value you supply and returning the accumulator’s final type directly.
  • reduce is fold without an explicit seed — it uses the iterator’s first element as the starting accumulator, so it returns Option<T> to account for an empty iterator.
  • Use fold when the accumulator type differs from the item type; use reduce only 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‘s Option result explicitly instead of defaulting to .unwrap(), which panics on an empty collection.