The Rust Expression Guide

Aggregating Data with fold

Last Updated: 2026-04-04

What this page is about

fold is one of the most important iterator methods in Rust because it captures a general pattern: start with some accumulator value, visit each input item in order, and produce one final result.

This page explains how that accumulator model works, how fold compares with manual loops, and when fold is the natural expression of the problem versus when it makes code denser and harder to read.

For most intermediate developers, the goal is not to replace every loop with fold. It is to recognize the kinds of aggregation where fold reveals the structure of the work clearly.

The core mental model

A useful way to think about fold is this: it carries forward a value that represents "everything we know so far."

Each input item updates that accumulator, and the final accumulator becomes the result.

In rough terms, the shape is:

iterator.fold(initial_accumulator, |acc, item| {
    // produce next accumulator from current accumulator and item
})

That means there are always three ingredients:

  • an initial accumulator value
  • a rule for combining the accumulator with one item
  • a final accumulated result

A very small example:

fn total(values: &[i32]) -> i32 {
    values.iter().fold(0, |acc, value| acc + value)
}

Here the accumulator is the running sum. It starts at 0, and each item contributes to the next total.

Why `fold` matters

Many loops are not really about iteration for its own sake. They are about building one final value from many inputs.

For example:

fn total_loop(values: &[i32]) -> i32 {
    let mut sum = 0;
    for value in values {
        sum += value;
    }
    sum
}

That is perfectly good code. But it is also exactly the shape that fold describes:

fn total_fold(values: &[i32]) -> i32 {
    values.iter().fold(0, |sum, value| sum + value)
}

The value of fold is not just compactness. It is that the code states the aggregation directly: start at 0, then combine each item into the running total.

A first comparison: loop versus `fold`

Consider a function that builds a comma-separated string from non-empty inputs.

Loop form:

fn joined_loop(values: &[&str]) -> String {
    let mut out = String::new();
 
    for value in values {
        let value = value.trim();
        if value.is_empty() {
            continue;
        }
 
        if !out.is_empty() {
            out.push_str(", ");
        }
        out.push_str(value);
    }
 
    out
}

fold form:

fn joined_fold(values: &[&str]) -> String {
    values
        .iter()
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .fold(String::new(), |mut out, value| {
            if !out.is_empty() {
                out.push_str(", ");
            }
            out.push_str(value);
            out
        })
}

The second version makes the aggregation boundary explicit: after normalization and filtering, one string is being built from many parts.

Whether it is better depends on how readable the accumulator logic feels. That judgment is an important part of using fold well.

The accumulator model in detail

The hardest part of learning fold is often building intuition about the accumulator. It helps to think of the accumulator as the current summary of everything processed so far.

For a sum, the accumulator is a number.

For a string build, the accumulator is the partially built string.

For a map build, the accumulator may be a collection being filled.

For example:

fn max_length(values: &[&str]) -> usize {
    values.iter().fold(0, |max_len, value| max_len.max(value.len()))
}

At each step:

  • max_len is the maximum length seen so far
  • value is the current item
  • the closure returns the next maximum length

This style gets easier when you stop thinking of fold as an abstract iterator trick and instead ask: what single value summarizes the processed prefix of the input?

When `fold` is elegant

fold is especially elegant when:

  • there is a clear accumulator type
  • each item contributes in the same general way
  • the combination rule is local and easy to read
  • the final result is genuinely one accumulated value

Examples include:

  • sums and products
  • min/max style calculations
  • building one string from many parts
  • counting or grouping under simple rules
  • building one collection from a stream of transformed items

For example:

fn total_bytes(lines: &[&str]) -> usize {
    lines.iter().fold(0, |sum, line| sum + line.len())
}

And:

fn longest_name(values: &[&str]) -> usize {
    values.iter().fold(0, |longest, value| longest.max(value.trim().len()))
}

These are good uses because the accumulator stays conceptually simple.

When `fold` becomes dense and unnatural

fold becomes less attractive when the accumulator logic is doing too many distinct things or when the accumulator itself stops being easy to reason about.

For example, this is legal Rust:

fn classify(values: &[&str]) -> (usize, usize, usize) {
    values.iter().fold((0, 0, 0), |(short, medium, long), value| {
        let len = value.trim().len();
        if len < 4 {
            (short + 1, medium, long)
        } else if len < 8 {
            (short, medium + 1, long)
        } else {
            (short, medium, long + 1)
        }
    })
}

This is not terrible, but the accumulator is already more cognitively demanding than a small struct or explicit loop might be.

A good rule is that fold should reveal the aggregation, not turn it into a puzzle.

Summing and counting

The simplest uses of fold are numerical aggregations.

fn sum(values: &[u32]) -> u32 {
    values.iter().fold(0, |sum, value| sum + value)
}

Counting selected items is also natural:

fn count_nonempty(values: &[&str]) -> usize {
    values
        .iter()
        .fold(0, |count, value| if value.trim().is_empty() { count } else { count + 1 })
}

That said, this second example also shows one of the judgment calls. Some developers may find filter(...).count() clearer:

fn count_nonempty_alt(values: &[&str]) -> usize {
    values.iter().filter(|value| !value.trim().is_empty()).count()
}

That is an important lesson: fold can express the operation, but it is not always the clearest specialized tool.

Building strings with `fold`

fold is often a nice fit when building one string from many pieces.

fn joined_paths(parts: &[&str]) -> String {
    parts.iter().fold(String::new(), |mut acc, part| {
        if !acc.is_empty() {
            acc.push('/');
        }
        acc.push_str(part.trim());
        acc
    })
}

This works well because the accumulator is just the string built so far.

Another example:

fn initials(names: &[&str]) -> String {
    names.iter().fold(String::new(), |mut out, name| {
        if let Some(first) = name.trim().chars().next() {
            out.push(first.to_ascii_uppercase());
        }
        out
    })
}

These are good examples because the final result is genuinely one accumulated string.

Building collections with `fold`

fold can also build collections.

fn normalized_names(values: &[&str]) -> Vec<String> {
    values.iter().fold(Vec::new(), |mut out, value| {
        let trimmed = value.trim();
        if !trimmed.is_empty() {
            out.push(trimmed.to_lowercase());
        }
        out
    })
}

This is valid and sometimes clear. But it is also the kind of example that raises an important question: is fold actually the best tool here, or would filter_map(...).collect() say more directly what is happening?

fn normalized_names_alt(values: &[&str]) -> Vec<String> {
    values
        .iter()
        .filter_map(|value| {
            let trimmed = value.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(trimmed.to_lowercase())
            }
        })
        .collect()
}

This comparison matters because fold is general, but more specialized methods are often clearer when they match the problem more precisely.

Meaningful examples

Example 1: sum parsed retry counts.

fn total_retries(values: &[u8]) -> u32 {
    values.iter().fold(0u32, |sum, &value| sum + value as u32)
}

Example 2: longest normalized username length.

fn longest_username(values: &[&str]) -> usize {
    values.iter().fold(0, |longest, value| longest.max(value.trim().len()))
}

Example 3: build a comma-separated label list.

fn labels(values: &[&str]) -> String {
    values.iter().fold(String::new(), |mut out, value| {
        let value = value.trim();
        if value.is_empty() {
            return out;
        }
        if !out.is_empty() {
            out.push_str(", ");
        }
        out.push_str(value);
        out
    })
}

Example 4: count enabled flags.

fn enabled_count(values: &[bool]) -> usize {
    values.iter().fold(0, |count, &enabled| if enabled { count + 1 } else { count })
}

A parsing example

Suppose a tool parses several lines and wants the total of all valid numeric values.

fn total_valid_numbers(lines: &[&str]) -> u32 {
    lines
        .iter()
        .filter_map(|line| line.trim().parse::<u32>().ok())
        .fold(0, |sum, n| sum + n)
}

This is a good example because fold is being used only for the aggregation part. Parsing and filtering are handled by methods that describe those steps more directly.

That separation is often a good sign. fold shines most when it is the natural final aggregation stage rather than the method being asked to carry the whole pipeline.

A configuration example

Configuration code often needs aggregate checks or summaries.

fn combined_flags(flags: &[&str]) -> String {
    flags
        .iter()
        .map(|flag| flag.trim())
        .filter(|flag| !flag.is_empty())
        .fold(String::new(), |mut acc, flag| {
            if !acc.is_empty() {
                acc.push(' ');
            }
            acc.push_str(flag);
            acc
        })
}

This is a reasonable use of fold because the code is building one combined string summary from many pieces.

Another example:

fn any_reserved_port(ports: &[u16]) -> bool {
    ports.iter().fold(false, |seen, &port| seen || port < 1024)
}

But this second example also teaches an important lesson: although fold can express the logic, any(|&port| port < 1024) is almost certainly clearer.

That contrast is useful because it helps show where fold is possible versus where it is natural.

A request-processing example

Request-processing code often builds summaries or aggregate values.

fn combined_roles(roles: &[String]) -> String {
    roles.iter().fold(String::new(), |mut acc, role| {
        let role = role.trim();
        if role.is_empty() {
            return acc;
        }
        if !acc.is_empty() {
            acc.push(',');
        }
        acc.push_str(role);
        acc
    })
}

This is a good fit because the output is one accumulated string.

But if the code needed to validate, deduplicate, and classify roles all inside the same accumulator closure, that would be a sign that the aggregation has become too dense for fold.

When `fold` is better than a loop

fold is often better than a loop when the loop is only maintaining one accumulator and the update rule is easy to see.

For example:

fn total_length(values: &[&str]) -> usize {
    values.iter().fold(0, |sum, value| sum + value.len())
}

And:

fn longest_line(values: &[&str]) -> usize {
    values.iter().fold(0, |longest, value| longest.max(value.len()))
}

In these cases, the loop version would not necessarily be wrong, but fold makes the accumulator pattern more explicit.

It says: this function is about reducing many inputs into one summary value.

When a loop or specialized method is better

A manual loop or a more specialized iterator method is often better when:

  • the accumulator is hard to understand
  • the closure contains substantial branching
  • the code needs side effects while aggregating
  • there is already a more direct method such as sum, count, any, all, or collect

For example:

fn total(values: &[u32]) -> u32 {
    values.iter().copied().sum()
}

This is clearer than using fold because sum names the operation directly.

Likewise:

fn any_blank(values: &[&str]) -> bool {
    values.iter().any(|value| value.trim().is_empty())
}

This is clearer than a boolean accumulator with fold.

A good rule is: use fold when it reveals the shape of the aggregation. Prefer a specialized method when one already expresses the operation more directly.

Readability and ownership with `fold`

One detail that often surprises developers is that fold usually takes ownership of the accumulator on each step and returns the next one.

That is why string and vector examples often use a mut binding inside the closure and then return it.

fn uppercase_words(words: &[&str]) -> Vec<String> {
    words.iter().fold(Vec::new(), |mut out, word| {
        out.push(word.to_uppercase());
        out
    })
}

This is normal, but it also affects readability. If the closure starts doing too much mutation before returning the accumulator, that is often a sign that a loop might communicate the same logic more clearly.

A small CLI example

Here is a small command-line example that parses numeric arguments and computes their total.

use std::env;
 
fn main() {
    let total = env::args()
        .skip(1)
        .filter_map(|arg| arg.parse::<i32>().ok())
        .fold(0, |sum, value| sum + value);
 
    println!("total: {total}");
}

You can try it with:

cargo run -- 1 2 3
cargo run -- 10 nope 5
cargo run

This is a good example because fold is clearly the final aggregation stage. The parsing step decides which values exist, and fold combines them into one total.

A small project file for experimentation

You can experiment with the examples in a small project like this:

[package]
name = "aggregating-data-with-fold"
version = "0.1.0"
edition = "2024"

Then place one example at a time in src/main.rs and run:

cargo run
cargo fmt
cargo clippy
cargo test

cargo fmt helps reveal the shape of accumulator closures. cargo clippy is useful for spotting cases where a more specialized method such as sum, count, or collect may be clearer than fold.

Common mistakes

There are a few recurring mistakes around fold.

First, using it for operations that already have a clearer specialized method such as sum, count, any, all, or collect.

Second, choosing an accumulator shape that is too hard to reason about.

Third, packing too much branching or business logic into the closure.

Fourth, using fold because it feels advanced rather than because it actually matches the problem.

Fifth, forgetting that a loop is often clearer when the accumulation process itself needs explanation.

The right goal is not to use fold often. The right goal is to recognize when one accumulated value really is the heart of the computation.

Refactoring patterns to watch for

When reviewing code, these are strong signals that fold may help:

  1. a loop updates one accumulator value on every iteration
  2. the final result is one summary value rather than a sequence of outputs
  3. the update rule is local and consistent across items
  4. the aggregation is the main story of the function

Signals that fold may be the wrong choice:

  1. the closure contains large branching logic
  2. the accumulator is a complicated tuple or ad hoc state machine
  3. a more specific iterator method already names the operation better
  4. the loop includes important side effects or debugging behavior

Typical before-and-after examples look like this:

fn before(values: &[u32]) -> u32 {
    let mut total = 0;
    for value in values {
        total += value;
    }
    total
}
 
fn after(values: &[u32]) -> u32 {
    values.iter().fold(0, |total, value| total + value)
}

And for string accumulation:

fn before(values: &[&str]) -> String {
    let mut out = String::new();
    for value in values {
        if !out.is_empty() {
            out.push(',');
        }
        out.push_str(value);
    }
    out
}
 
fn after(values: &[&str]) -> String {
    values.iter().fold(String::new(), |mut out, value| {
        if !out.is_empty() {
            out.push(',');
        }
        out.push_str(value);
        out
    })
}

Key takeaways

fold is a general aggregation tool for building one result from many inputs.

The main ideas from this page are:

  • think of fold in terms of an accumulator that summarizes everything processed so far
  • it is most elegant when the accumulator is simple and the update rule is easy to read
  • it works well for sums, maxima, string building, and other clear one-value aggregations
  • it becomes dense and unnatural when the accumulator or branching logic becomes hard to reason about
  • a loop is still often better when the accumulation process itself needs explanation
  • specialized methods such as sum, count, any, all, and collect are often clearer when they fit the problem directly

The real value of fold is not that it can express many things. It is that, in the right cases, it expresses aggregation in exactly the shape the problem already has.