How to Use fold and reduce in Rust

Use fold to accumulate values with a starting point and reduce to combine all elements into a single result without an initial value.

When a simple sum isn't enough

You are building a dashboard that aggregates sensor readings. You need to sum up temperatures, but the list might be empty. If the list is empty, sum returns zero, which is fine. But what if you need to calculate an average? You need the sum and the count. Or what if you're building a string from parts, and you need to start with a specific prefix? You can't just mash elements together; you need a starting point. That's where fold steps in. And when you don't need a starting point, but just want to collapse a list into one value, reduce is the tool.

Rust gives you fold and reduce to collapse an iterator into a single value. They look similar, but they solve different problems. fold carries an initial value through the iteration. reduce uses the first element of the iterator as the starting point. The difference changes how you handle empty collections, how types flow, and how you write the closure.

The accumulator pattern

Think of fold as a factory conveyor belt. At the start of the belt, you place a raw material block. That's your initial accumulator. Every item that passes through the station gets welded onto that block. By the time the block reaches the end, it holds everything. The block exists even if no items pass through. The result is always the block, modified or not.

reduce is a knockout tournament. You take the first two fighters, they battle, the winner moves on. The winner fights the next item. The last one standing is the result. If there are no fighters, there's no winner. The result is optional.

This distinction drives the API. fold takes an initial value. reduce does not. fold returns the accumulator type. reduce returns Option<T> because the iterator might be empty.

Minimal examples

Here is the basic usage. fold starts with 0. reduce starts with the first element.

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

    // fold: starts with 0, adds each number.
    // The 0 is the initial value. If the list is empty, the result is 0.
    let sum_fold: i32 = numbers.iter().fold(0, |acc, &x| acc + x);

    // reduce: combines elements pairwise.
    // Returns Option because an empty list has no first element to start with.
    let sum_reduce: Option<i32> = numbers.iter().reduce(|acc, &x| acc + x);

    println!("Fold: {}, Reduce: {:?}", sum_fold, sum_reduce);
}

The output is Fold: 10, Reduce: Some(10). Both compute the sum. fold gives you an i32. reduce gives you an Option<i32>. The Option forces you to handle the case where the list is empty. If numbers were empty, sum_fold would be 0. sum_reduce would be None.

Tracing the execution

Understanding the step-by-step behavior clarifies when to pick which method.

fold calls the closure once per item. The closure receives the current accumulator and the current item. It returns the new accumulator. The return value of the closure becomes the accumulator for the next step.

For numbers.iter().fold(0, |acc, &x| acc + x):

  • Initial accumulator is 0.
  • Item 1: closure gets acc=0, x=1. Returns 1.
  • Item 2: closure gets acc=1, x=2. Returns 3.
  • Item 3: closure gets acc=3, x=3. Returns 6.
  • Item 4: closure gets acc=6, x=4. Returns 10.
  • Result is 10.

reduce calls the closure N-1 times for N items. It borrows the first item to initialize the accumulator. The closure receives the current accumulator and the next item.

For numbers.iter().reduce(|acc, &x| acc + x):

  • Iterator yields 1. reduce uses 1 as the initial accumulator.
  • Item 2: closure gets acc=1, x=2. Returns 3.
  • Item 3: closure gets acc=3, x=3. Returns 6.
  • Item 4: closure gets acc=6, x=4. Returns 10.
  • Result is Some(10).

If the list is empty, reduce never gets a first item. It returns None immediately. The closure never runs.

The accumulator carries the history; the iterator provides the fuel.

Real-world aggregation

fold shines when you need to carry state that isn't in the collection. You are calculating a running balance. The opening balance is external. The transactions are the list.

#[derive(Debug)]
struct Transaction {
    amount: i64,
    description: String,
}

fn main() {
    let transactions = vec![
        Transaction { amount: 100, description: "Salary".to_string() },
        Transaction { amount: -20, description: "Coffee".to_string() },
        Transaction { amount: -5, description: "Bus".to_string() },
    ];

    // Start with an opening balance of 50.
    // fold lets you carry state that isn't in the list.
    let final_balance = transactions.iter().fold(50, |balance, tx| {
        balance + tx.amount
    });

    println!("Final balance: {}", final_balance);
}

The initial value 50 sets the type of the accumulator to i64. The closure adds tx.amount to the balance. The result is 125. If you used reduce, you couldn't include the opening balance. reduce would only combine transactions. You would have to add the balance after the fact. fold keeps the logic in one place.

Another common pattern is building a complex structure. You have a list of config overrides. You want to merge them into a default config. fold starts with the default. Each override updates the config.

#[derive(Clone, Debug)]
struct Config {
    verbose: bool,
    threads: usize,
}

fn main() {
    let default = Config { verbose: false, threads: 1 };
    let overrides = vec![
        ("verbose", "true"),
        ("threads", "4"),
    ];

    // fold merges overrides into the default config.
    // The accumulator is the Config struct.
    let final_config = overrides.iter().fold(default, |config, (key, value)| {
        match *key {
            "verbose" => Config { verbose: value == "true", ..config },
            "threads" => Config { threads: value.parse().unwrap_or(config.threads), ..config },
            _ => config,
        }
    });

    println!("{:?}", final_config);
}

The closure matches the key and returns a new Config with the updated field. The accumulator flows through the list. The result is the merged config. External state belongs in the initial value, not hidden in a mutable variable.

The secret weapon: try_fold

fold and reduce always iterate to the end. If you have a million items, they process all million. Sometimes you want to stop early. You are looking for the first negative number. Once you find it, you can stop. try_fold is the method for this.

try_fold works like fold, but the closure returns Option<T> or Result<T, E>. If the closure returns Some(value) or Ok(value), iteration continues with value as the accumulator. If the closure returns None or Err(e), iteration stops immediately. The result is the None or Err(e).

This is how any, all, and find are implemented. try_fold is the engine under the hood.

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

    // try_fold allows early exit.
    // Returns Option. None means short-circuited.
    let first_negative = numbers.iter().try_fold((), |(), &x| {
        if x < 0 {
            // Return None to stop iteration.
            // The accumulator type is (), so we return None::<()>.
            None
        } else {
            // Return Some to continue.
            Some(())
        }
    });

    // first_negative is None because we found -3.
    // If the list had no negatives, it would be Some(()).
    println!("Found negative: {}", first_negative.is_none());
}

The closure checks x < 0. If true, it returns None. try_fold stops and returns None. The remaining items 4 and 5 are never processed. This is efficient for large datasets. try_fold gives you full control over short-circuiting.

Pitfalls and compiler errors

Type inference can trip you up. fold needs the initial value to determine the accumulator type. If you write numbers.iter().fold(0, |acc, x| acc + x), the compiler infers i32 from 0. If you want i64, you must write fold(0i64, ...) or annotate the variable.

If you assign reduce to a variable without Option, the compiler rejects you with E0308 (mismatched types). You expected i32, found Option<i32>. You have to unwrap or handle the None.

let sum = numbers.iter().reduce(|acc, &x| acc + x);
// Error[E0308]: mismatched types
// expected `i32`, found `Option<i32>`

Convention: prefer fold over reduce in most cases. reduce forces Option handling, which adds noise. fold lets you choose the default. If you need "empty means nothing", use reduce. Otherwise, fold is more flexible.

Another pitfall is moving values. fold takes ownership of the iterator. If you iterate over owned values, the closure receives owned values. If you iterate over references, the closure receives references. Use pattern matching to dereference cleanly.

// Iterating over &i32 yields &&i32.
// Pattern matching avoids double references.
let sum = numbers.iter().fold(0, |acc, &x| acc + x);

// Without pattern matching, you'd write:
let sum = numbers.iter().fold(0, |acc, x| acc + *x);

Convention: use &x in the closure pattern when iterating over references. It's cleaner than *x.

If the compiler asks for an Option, listen. It's protecting you from a null crash.

Decision matrix

Use fold when you need an initial value that isn't part of the collection. Use fold when the result type differs from the element type, like summing numbers into a string representation. Use fold when you want a guaranteed result even if the collection is empty. Use reduce when the result type matches the element type and an empty collection should yield no result. Use reduce when you are implementing a mathematical operation like finding the maximum or minimum where an empty set is undefined. Use sum or product when you are just adding or multiplying numbers; the specialized methods are faster and clearer. Use try_fold when you need to short-circuit the iteration based on a condition. Use fold for control, reduce for purity, and built-ins for speed.

Where to go next