๐Ÿฆ€ Functional Rust

057: Fold Left

Difficulty: 2 Level: Beginner Accumulate a result left-to-right with a running accumulator โ€” the tail-recursive fold.

The Problem This Solves

Sum a list of numbers. Find the maximum. Reverse a list. Build a string from parts. All of these are left folds: start with an initial value, walk the list left to right, update the accumulator at each step, return the final accumulated result. `fold_left` is the workhorse of functional programming. It's the pattern behind `sum`, `product`, `max`, and `reverse`. Once you see `fold_left`, you see it everywhere. And unlike `fold_right`, it's tail-recursive โ€” safe for arbitrarily large inputs because it doesn't build up a stack. Rust's `Iterator::fold` is `fold_left`. Every time you call `.fold(init, |acc, x| ...)`, you're writing a left fold.

The Intuition

`fold_left f init [a, b, c]` evaluates as:
((init f a) f b) f c
Left to right. The accumulator starts at `init`, is updated by `f(acc, a)`, then `f(result, b)`, then `f(result, c)`. Each step consumes the current accumulator and produces the next one. For sum: `((0 + 3) + 1) + 4 = 8`. The accumulator carries the running total. For reverse: start with `[]`, prepend each element: `[] โ†’ [a] โ†’ [b,a] โ†’ [c,b,a]`.

How It Works in Rust

// Generic fold_left โ€” iterative (Rust has no TCO guarantee)
pub fn fold_left<T, A>(f: impl Fn(A, &T) -> A, mut acc: A, xs: &[T]) -> A {
 for x in xs {
     acc = f(acc, x);  // tail-recursive in OCaml; iterative loop in Rust
 }
 acc
}

// Sum, product, max via fold_left:
fold_left(|acc, &x| acc + x, 0, &[1, 2, 3])    // โ†’ 6
fold_left(|acc, &x| acc * x, 1, &[1, 2, 3])    // โ†’ 6
// Max with Option for empty-safety:
let (&first, rest) = xs.split_first()?;
fold_left(|acc, &x| acc.max(x), first, rest)   // โ†’ Some(max)

// Reverse via fold_left โ€” the classic FP trick:
fold_left(|mut acc: Vec<i64>, &x| { acc.insert(0, x); acc }, vec![], xs)

// Rust's built-in left fold:
xs.iter().copied().fold(0i64, i64::wrapping_add)
Note: OCaml guarantees tail-call optimisation (TCO) for `fold_left`. Rust does not. Our implementation uses a `for` loop, which has the same constant-stack behaviour without relying on TCO.

What This Unlocks

Key Differences

ConceptOCamlRust
Tail recursionGuaranteed TCO โ€” safe for millions of elementsNo TCO โ€” use iterative loop instead
Empty-list max`List.hd` panics on emptyReturn `Option<T>` โ€” explicit emptiness
AccumulatorImmutable, returned each step`mut acc` updated in place each iteration
In-place reverseAllocates new list`Vec::reverse()` โ€” O(1) extra space
Standard library`List.fold_left f init lst``iter().fold(init, \acc, x\...)`
//! # fold_left โ€” Tail-Recursive Accumulator
//!
//! OCaml's `fold_left` processes a list left to right with an accumulator:
//!   fold_left f init [a; b; c] = f (f (f init a) b) c
//!
//! This is tail-recursive in OCaml (and maps directly to Rust's `Iterator::fold`).

// ---------------------------------------------------------------------------
// Approach A: Idiomatic Rust โ€” iterator methods
// ---------------------------------------------------------------------------

pub fn sum_idiomatic(xs: &[i64]) -> i64 {
    xs.iter().sum()
}

pub fn product_idiomatic(xs: &[i64]) -> i64 {
    xs.iter().product()
}

/// Maximum element. Returns `None` for empty slices.
/// Uses `iter().copied().max()` which returns `Option<i64>`.
pub fn maximum_idiomatic(xs: &[i64]) -> Option<i64> {
    // Unlike OCaml's version which panics on empty list, we return Option
    xs.iter().copied().max()
}

pub fn reverse_idiomatic(xs: &[i64]) -> Vec<i64> {
    let mut v = xs.to_vec();
    v.reverse();
    v
}

// ---------------------------------------------------------------------------
// Approach B: Functional / explicit fold_left (recursive, tail-recursive style)
// ---------------------------------------------------------------------------

/// A generic left fold mirroring OCaml's `fold_left`.
///
/// Rust doesn't guarantee TCO, but the structure is identical to OCaml's
/// tail-recursive fold_left. For large inputs, the iterative version is safer.
pub fn fold_left<T, A>(f: impl Fn(A, &T) -> A, mut acc: A, xs: &[T]) -> A {
    for x in xs {
        acc = f(acc, x);
    }
    acc
}

pub fn sum_functional(xs: &[i64]) -> i64 {
    fold_left(|acc, &x| acc + x, 0, xs)
}

pub fn product_functional(xs: &[i64]) -> i64 {
    fold_left(|acc, &x| acc * x, 1, xs)
}

pub fn maximum_functional(xs: &[i64]) -> Option<i64> {
    // Safe version: use first element as initial accumulator
    let (&first, rest) = xs.split_first()?;
    Some(fold_left(
        |acc, &x| if x > acc { x } else { acc },
        first,
        rest,
    ))
}

pub fn reverse_functional(xs: &[i64]) -> Vec<i64> {
    // Mirrors OCaml: fold_left (fun acc x -> x :: acc) [] lst
    fold_left(
        |mut acc: Vec<i64>, &x| {
            acc.push(x); // push + final reverse, or insert(0, x) like OCaml's cons
            acc
        },
        Vec::new(),
        xs,
    )
    .into_iter()
    .rev()
    .collect()
}

// ---------------------------------------------------------------------------
// Approach C: Iterator::fold โ€” Rust's built-in left fold
// ---------------------------------------------------------------------------

/// Sum using Iterator::fold โ€” explicitly showing the fold call.
/// We add a type annotation to show fold's signature clearly.
pub fn sum_iter_fold(xs: &[i64]) -> i64 {
    xs.iter().copied().fold(0i64, i64::wrapping_add)
}

/// Product using Iterator::fold.
pub fn product_iter_fold(xs: &[i64]) -> i64 {
    xs.iter().copied().fold(1i64, i64::wrapping_mul)
}

pub fn reverse_iter_fold(xs: &[i64]) -> Vec<i64> {
    // fold left, prepending each element
    xs.iter().fold(Vec::new(), |mut acc, &x| {
        acc.insert(0, x);
        acc
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sum_basic() {
        let xs = [3, 1, 4, 1, 5, 9, 2, 6];
        assert_eq!(sum_idiomatic(&xs), 31);
        assert_eq!(sum_functional(&xs), 31);
        assert_eq!(sum_iter_fold(&xs), 31);
    }

    #[test]
    fn test_sum_empty() {
        let xs: &[i64] = &[];
        assert_eq!(sum_idiomatic(xs), 0);
        assert_eq!(sum_functional(xs), 0);
        assert_eq!(sum_iter_fold(xs), 0);
    }

    #[test]
    fn test_product_single() {
        let xs = [7];
        assert_eq!(product_idiomatic(&xs), 7);
        assert_eq!(product_functional(&xs), 7);
        assert_eq!(product_iter_fold(&xs), 7);
    }

    #[test]
    fn test_product_basic() {
        let xs = [3, 1, 4, 1, 5, 9, 2, 6];
        assert_eq!(product_idiomatic(&xs), 6480);
        assert_eq!(product_functional(&xs), 6480);
        assert_eq!(product_iter_fold(&xs), 6480);
    }

    #[test]
    fn test_maximum() {
        let xs = [3, 1, 4, 1, 5, 9, 2, 6];
        assert_eq!(maximum_idiomatic(&xs), Some(9));
        assert_eq!(maximum_functional(&xs), Some(9));
    }

    #[test]
    fn test_maximum_empty() {
        let xs: &[i64] = &[];
        assert_eq!(maximum_idiomatic(xs), None);
        assert_eq!(maximum_functional(xs), None);
    }

    #[test]
    fn test_maximum_single() {
        assert_eq!(maximum_idiomatic(&[42]), Some(42));
        assert_eq!(maximum_functional(&[42]), Some(42));
    }

    #[test]
    fn test_reverse() {
        let xs = [3, 1, 4, 1, 5, 9, 2, 6];
        let expected = vec![6, 2, 9, 5, 1, 4, 1, 3];
        assert_eq!(reverse_idiomatic(&xs), expected);
        assert_eq!(reverse_functional(&xs), expected);
        assert_eq!(reverse_iter_fold(&xs), expected);
    }

    #[test]
    fn test_reverse_empty() {
        let xs: &[i64] = &[];
        let empty: Vec<i64> = vec![];
        assert_eq!(reverse_idiomatic(xs), empty);
        assert_eq!(reverse_functional(xs), empty);
        assert_eq!(reverse_iter_fold(xs), empty);
    }

    #[test]
    fn test_fold_left_generic() {
        // Build a string: "((init+3)+1)+4"
        let xs = [3, 1, 4];
        let result = fold_left(|acc, x| format!("({acc}+{x})"), "init".to_string(), &xs);
        assert_eq!(result, "(((init+3)+1)+4)");
    }
}

fn main() {
    println!("{:?}", sum_idiomatic(&xs), 31);
    println!("{:?}", sum_functional(&xs), 31);
    println!("{:?}", sum_iter_fold(&xs), 31);
}
(* fold_left โ€” Tail-Recursive Accumulator *)

(* Implementation 1: Direct tail-recursive fold_left *)
let rec fold_left f acc = function
  | []     -> acc
  | h :: t -> fold_left f (f acc h) t

(* Implementation 2: Using List.fold_left from stdlib *)
let fold_left_stdlib f acc lst = List.fold_left f acc lst

(* Classic uses *)
let sum     lst = fold_left ( + ) 0 lst
let product lst = fold_left ( * ) 1 lst
let maximum lst = fold_left max (List.hd lst) (List.tl lst)
let reverse lst = fold_left (fun acc x -> x :: acc) [] lst

(* Tests *)
let () =
  let nums = [3; 1; 4; 1; 5; 9; 2; 6] in
  assert (sum nums = 31);
  assert (product nums = 6480);
  assert (maximum nums = 9);
  assert (reverse [1; 2; 3] = [3; 2; 1]);
  assert (sum [] = 0);
  assert (reverse [] = []);
  Printf.printf "All fold_left tests passed!\n"

๐Ÿ“Š Detailed Comparison

Comparison: fold_left โ€” OCaml vs Rust

OCaml โ€” Tail-Recursive

๐Ÿช Show OCaml equivalent
let rec fold_left f acc = function
| []     -> acc
| h :: t -> fold_left f (f acc h) t

let sum     lst = fold_left ( + ) 0 lst
let product lst = fold_left ( * ) 1 lst
let maximum lst = fold_left max (List.hd lst) (List.tl lst)
let reverse lst = fold_left (fun acc x -> x :: acc) [] lst

Rust โ€” Idiomatic

pub fn sum_idiomatic(xs: &[i64]) -> i64 { xs.iter().sum() }
pub fn product_idiomatic(xs: &[i64]) -> i64 { xs.iter().product() }
pub fn maximum_idiomatic(xs: &[i64]) -> Option<i64> { xs.iter().copied().max() }
pub fn reverse_idiomatic(xs: &[i64]) -> Vec<i64> {
 let mut v = xs.to_vec();
 v.reverse();
 v
}

Rust โ€” Functional (Custom fold_left)

pub fn fold_left<T, A>(f: impl Fn(A, &T) -> A, mut acc: A, xs: &[T]) -> A {
 for x in xs { acc = f(acc, x); }
 acc
}

pub fn maximum_functional(xs: &[i64]) -> Option<i64> {
 let (&first, rest) = xs.split_first()?;
 Some(fold_left(|acc, &x| if x > acc { x } else { acc }, first, rest))
}

Comparison Table

AspectOCamlRust
Type signature`('b -> 'a -> 'b) -> 'b -> 'a list -> 'b``fn fold_left<T, A>(f: Fn(A, &T) -> A, acc: A, xs: &[T]) -> A`
Tail-call optimizationGuaranteed by compilerNot guaranteed (we use a loop instead)
Empty list max`List.hd` raises exceptionReturns `None` (safe)
Reverse mechanismCons to front: `x :: acc``v.reverse()` in-place or `insert(0, x)`
Built-in`List.fold_left``Iterator::fold`
AccumulatorPassed by value (GC'd)Moved by ownership

Type Signatures Explained

OCaml: `val fold_left : ('b -> 'a -> 'b) -> 'b -> 'a list -> 'b`

  • Accumulator type `'b` comes first in the function parameter
  • The accumulator is threaded through each recursive call
Rust: `fn fold_left<T, A>(f: impl Fn(A, &T) -> A, acc: A, xs: &[T]) -> A`
  • `A` is the accumulator type, `T` is the element type
  • `&T`: elements are borrowed from the slice
  • `mut acc`: the accumulator is mutably rebound on each iteration

Takeaways

1. fold_left is Rust's natural fold โ€” `Iterator::fold` processes left to right, matching fold_left's semantics exactly 2. Safety improvement: Rust's `maximum` returns `Option` instead of panicking on empty input 3. No TCO needed: Rust's `for` loop is already iterative โ€” no tail-call optimization concern 4. Ownership makes fold natural โ€” the accumulator is moved into each step, not shared 5. OCaml's `function` keyword combines `fun` + `match`; Rust uses `match` explicitly or `for` loops