How to use fold and reduce

Use fold to accumulate values with a starting point and reduce to combine iterator items into a single result.

When loops get messy

You are parsing a stream of API response times. You need the average latency, but you also need to flag if any single request spiked over 500ms. A for loop works, but it scatters your logic across mutable variables, index tracking, and early breaks. You end up with a block of code that mutates state in three different places and requires careful reading to verify correctness.

Rust's iterator chain offers a cleaner path. You can accumulate the sum, track the maximum, and filter spikes in a single pass without touching mutable state. That is where fold and reduce live. They turn a sequence of items into a single result by applying a combining rule repeatedly. The difference between them comes down to one question: do you control the starting value, or does the iterator?

Fold is the accumulator

fold gives you total control. You provide a starting value, called the seed. The iterator hands you each item one by one. You combine the item with your accumulator and pass the result forward. The accumulator can be any type. You can start with an integer and end with a string. You can start with a vector and end with a map. fold does not care about the relationship between the item type and the result type. It only cares that your closure returns the same type as the seed.

Think of fold like a conveyor belt with a bucket at the start. You hold the bucket. As items pass, you decide what goes in. You might weigh them, count them, or smash them together. When the belt stops, you hand back the bucket. The bucket can be different from the items. The items can be apples; the bucket can hold a weight measurement.

The seed dictates the return type of fold. This is a powerful feature for type inference. If you pass 0 as the seed, the compiler knows the result is an integer. If you pass String::new(), the result is a string. The closure must return that same type, or the compiler rejects you with E0308 (mismatched types).

Reduce is the combiner

reduce is more restrictive. It does not take a seed. It grabs the first item from the iterator and uses that as the starting point. Then it combines the rest. The result must be the same type as the items. If you reduce a list of integers, you get an integer. If you reduce a list of strings, you get a string.

There is a catch. If the iterator is empty, reduce has nothing to grab. It cannot invent a starting value. So reduce returns Option<T>. An empty iterator yields None. A non-empty iterator yields Some(result).

Use reduce when the operation is naturally defined over the items themselves and an empty list means "no result." Finding the maximum value in a list is a classic case. There is no universal "smallest number" that works as a seed for all types, so reduce handles the empty case gracefully by returning None.

Minimal examples

fn main() {
    let numbers = vec![1, 2, 3, 4];

    // fold: You provide the seed (0). The closure receives (accumulator, item).
    // The result type matches the seed type, not necessarily the item type.
    // iter() yields &i32. &x destructures the reference to get i32.
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Sum via fold: {}", sum);

    // reduce: No seed. Takes the first item as the start.
    // Returns Option because the iterator might be empty.
    // iter() yields &i32. reduce expects the closure to return &i32.
    // Use copied() to work with owned values for arithmetic.
    let product = numbers.iter().copied().reduce(|acc, x| acc * x);
    println!("Product via reduce: {:?}", product);
}

The convention here is explicit. fold takes a seed. reduce does not. reduce returns an Option. When working with iter(), which yields references, reduce is awkward for arithmetic because you cannot return a new value from a closure that expects a reference. The community standard is to chain .copied() or .cloned() before reduce when you need owned values. This avoids lifetime headaches and makes the intent clear.

Tracing the execution

Understanding the step-by-step flow prevents bugs. Trace fold with seed 0 and items [1, 2, 3].

Step 1: The accumulator is 0. The iterator yields 1. The closure runs 0 + 1. It returns 1. Step 2: The accumulator is 1. The iterator yields 2. The closure runs 1 + 2. It returns 3. Step 3: The accumulator is 3. The iterator yields 3. The closure runs 3 + 3. It returns 6. The iterator is exhausted. fold returns 6.

Trace reduce with [1, 2, 3]. Step 1: The iterator yields 1. reduce uses 1 as the initial accumulator. Step 2: The iterator yields 2. The closure runs 1 * 2. It returns 2. Step 3: The iterator yields 3. The closure runs 2 * 3. It returns 6. The iterator is exhausted. reduce returns Some(6).

Trace reduce with an empty iterator. The iterator yields nothing. reduce returns None immediately. No closure runs.

The seed is your contract. Pick it wrong and the compiler won't let you lie.

Realistic use: Building a frequency map

fold shines when you need to build a complex structure from a stream. Imagine counting word frequencies in a log file. You start with an empty HashMap. For each word, you update the map. You end with a populated map. The types change from &str to HashMap<String, usize>. A for loop can do this, but fold keeps the map creation and population in one expression.

use std::collections::HashMap;

/// Counts occurrences of each word in the input text.
/// Returns a map from lowercase word to count.
fn count_words(text: &str) -> HashMap<String, usize> {
    text.split_whitespace()
        // fold builds the HashMap. The seed is an empty map.
        // The closure mutates the map and returns it.
        .fold(HashMap::new(), |mut counts, word| {
            // Increment the count for this word.
            // entry API avoids double hashing.
            *counts.entry(word.to_lowercase()).or_insert(0) += 1;
            counts
        })
}

fn main() {
    let log = "error warning error info error";
    let frequencies = count_words(log);
    println!("{:?}", frequencies);
}

The closure takes mut counts because you need to modify the map. The closure must return counts at the end. This pattern is common: mutate the accumulator, then return it. The compiler enforces that you return the accumulator. If you forget the return, you get a type mismatch error.

Build the map. Return the map. Let the iterator handle the loop.

The borrowing trap in closures

Closures in fold and reduce often trip up beginners because of references. iter() yields &T. Your closure receives &T. If you try to use the value directly, you might move it out of a reference, which triggers E0507 (cannot move out of borrowed content).

The fix is destructuring or copying. In |acc, &x|, the &x pattern destructures the reference, giving you x as an owned value if T is Copy. For integers, this works perfectly. For strings, you need .to_string() or .to_owned().

When using reduce with iter(), the trap is deeper. reduce expects the closure to return the same type as the iterator items. If the iterator yields &i32, the closure must return &i32. You cannot return acc + x because that creates a new i32, not a reference. You must return a reference to something that lives long enough. This is why reduce is rarely used directly on iter() for arithmetic. Chain .copied() to convert &i32 to i32, and reduce works naturally.

Handle the None. An empty list is a valid list, not a bug.

Pitfalls and compiler errors

E0308: Mismatched types. This is the most common error with fold. Your seed is i32, but your closure returns i64. Or your seed is String, but the closure returns &str. The compiler infers the return type of fold from the seed. The closure must match. Fix this by aligning the seed type with the closure return type.

The empty iterator trap. reduce returns Option. If you call .unwrap() on the result without checking, your program panics on empty input. Always handle None. Use .unwrap_or(default) if you have a sensible fallback, or match on the option.

Performance myths. fold and reduce compile to tight loops. There is no runtime overhead compared to a for loop. The compiler inlines the closure and optimizes the loop. Do not avoid fold for performance reasons. Use it for clarity.

Associativity matters. reduce processes items left-to-right. If your operation is not associative, the order matters. (a - b) - c is not the same as a - (b - c). reduce always goes left-to-right. If you need right-to-left, reverse the iterator first.

Decision matrix

Use fold when you need to accumulate into a different type than the iterator yields. Use fold when you need a guaranteed result even if the iterator is empty, because you control the seed. Use fold when you are building a complex structure like a HashMap or a custom struct from a stream of items.

Use reduce when you are combining items of the same type and the result should be that same type. Use reduce when an empty iterator naturally means "no result," so returning Option fits your logic. Use reduce for simple algebraic operations like finding the maximum or minimum where the identity element is hard to define or irrelevant.

Use sum() or product() when you are adding or multiplying numbers. These methods are specialized, faster, and handle the seed implicitly. The community prefers numbers.iter().sum() over fold(0, |a, b| a + b).

Use a for loop when you need side effects, early breaks, or complex control flow that a closure can't express cleanly. fold and reduce are for pure accumulation. If you need to mutate external state or break early based on complex conditions, a loop is clearer.

Pick the tool that matches your data shape. If the types change, fold. If they stay the same, reduce.

Where to go next