๐Ÿฆ€ Functional Rust

068: Tail-Recursive Accumulator

Difficulty: 2 Level: Intermediate Transform naive recursion into tail recursion by passing an accumulator โ€” and understand why Rust prefers iterators.

The Problem This Solves

Sum a list of a million numbers recursively and you'll get a stack overflow. Each recursive call pushes a stack frame, and the call stack has a hard limit (typically 8MB). Naive `sum([1, 2, ..., 1_000_000])` tries to push a million frames. The classic fix is the accumulator pattern: instead of returning a value and adding to it on the way back up the call stack, carry a running total as a parameter on the way down. The final recursive call can then return the accumulator directly โ€” no work left to do on the return path. This is a tail call, and a compiler that implements tail-call optimization (TCO) can reuse the same stack frame, making the recursion as safe as a loop. OCaml guarantees TCO for tail-recursive functions. Rust does not. So in Rust, the accumulator pattern teaches the concept, but for production code you should use iterators (`list.iter().sum()`) or explicit loops. This example shows both, explains why, and builds the transferable skill of recognizing when a function is tail-recursive.

The Intuition

Non-tail-recursive sum: `sum([1, 2, 3]) = 1 + sum([2, 3]) = 1 + (2 + sum([3])) = 1 + (2 + (3 + sum([])))`. The additions happen after returning from the recursive calls. Each level waits for the level below before it can compute. Stack depth = list length. Tail-recursive sum with accumulator: `sum_tr(acc=0, [1, 2, 3]) = sum_tr(acc=1, [2, 3]) = sum_tr(acc=3, [3]) = sum_tr(acc=6, []) = 6`. The recursive call is the last thing that happens โ€” no work left after returning. With TCO, this becomes a `goto` to the top of the function, reusing the same stack frame. The pattern to recognize: a function is tail-recursive if every recursive call is in tail position โ€” the result of the call is returned directly, not used in a further computation. In Rust without TCO guarantee, the accumulator pattern is still instructive because: (1) it shows you how to think about computation in terms of state that flows forward rather than results that accumulate backward, and (2) it's directly translatable to an explicit loop.

How It Works in Rust

Naive (not tail-recursive โ€” dangerous for large lists):
pub fn sum_naive(list: &[i64]) -> i64 {
 match list {
     [] => 0,
     [h, rest @ ..] => h + sum_naive(rest),  // + happens AFTER return โ€” not tail position
 }
}
Tail-recursive style (accumulator carries state forward):
pub fn sum_tr(list: &[i64]) -> i64 {
 fn go(acc: i64, slice: &[i64]) -> i64 {
     match slice {
         [] => acc,                         // return accumulator directly
         [h, rest @ ..] => go(acc + h, rest), // last operation: recursive call only
     }
 }
 go(0, list)  // start with acc=0
}
The idiomatic Rust version (what you'd actually write):
pub fn sum_iter(list: &[i64]) -> i64 { list.iter().sum() }
Reverse as accumulator pattern (OCaml's `h :: acc` prepend โ†’ Rust's push + reverse):
pub fn rev_tr<T: Clone>(list: &[T]) -> Vec<T> {
 // Idiomatic: iterators handle this cleanly
 list.iter().rev().cloned().collect()
}
When you really need the recursive accumulator style (e.g., teaching or implementing without std), convert to a loop:
// The tail-recursive pattern translates directly to a loop:
pub fn sum_loop(list: &[i64]) -> i64 {
 let mut acc = 0;
 let mut rest = list;
 loop {
     match rest {
         [] => return acc,
         [h, tail @ ..] => { acc += h; rest = tail; }
     }
 }
}

What This Unlocks

Key Differences

ConceptOCamlRust
TCO guaranteeYes โ€” tail calls are optimizedNo โ€” LLVM may optimize, not guaranteed
Accumulator patternSame structure; safe due to TCOInstructive but use iterators for safety
`List.rev`Uses `rev_append` (tail-recursive)`iter().rev().cloned().collect()`
Preferred idiomTail-recursive helpers with `go``iter().sum()`, `iter().fold()`, explicit loops
Stack overflow riskEliminated by TCO for tail callsStill present; default stack ~8MB
/// Tail-Recursive Accumulator Pattern
///
/// OCaml relies on tail-call optimization (TCO) for stack safety.
/// Rust does NOT guarantee TCO, so idiomatic Rust uses iterators
/// or explicit loops. We show both styles for comparison.

/// Naive recursive sum โ€” would blow the stack on large inputs in both languages.
pub fn sum_naive(list: &[i64]) -> i64 {
    match list {
        [] => 0,
        [h, rest @ ..] => h + sum_naive(rest),
    }
}

/// Tail-recursive style using an explicit loop (since Rust has no TCO guarantee).
pub fn sum_tr(list: &[i64]) -> i64 {
    let mut acc = 0i64;
    let mut slice = list;
    loop {
        match slice {
            [] => return acc,
            [h, rest @ ..] => {
                acc += h;
                slice = rest;
            }
        }
    }
}

/// Idiomatic Rust: use iterators. This is the preferred approach.
pub fn sum_iter(list: &[i64]) -> i64 {
    list.iter().sum()
}

/// Tail-recursive reverse โ€” process from front, collect into Vec in reverse.
/// OCaml's `h :: acc` prepends; Rust's Vec::insert(0, h) is O(n) per call.
/// We use a VecDeque-style push_front or simply iterate backwards.
pub fn rev_tr<T: Clone>(list: &[T]) -> Vec<T> {
    // Functional accumulator style: fold from left, building reversed result
    list.iter().rev().cloned().collect()
}

/// Explicit recursive version mirroring OCaml's pattern.
pub fn rev_recursive<T: Clone>(list: &[T]) -> Vec<T> {
    fn go<T: Clone>(acc: &mut Vec<T>, slice: &[T]) {
        match slice {
            [] => {}
            [h, rest @ ..] => {
                // In OCaml: h :: acc (prepend). In Rust we build reversed by
                // inserting at front โ€” but that's O(n). Instead we push and
                // the recursion order naturally reverses.
                go(acc, rest);
                acc.push(h.clone());
            }
        }
    }
    let mut result = Vec::new();
    go(&mut result, list);
    result
}

/// Idiomatic Rust reverse.
pub fn rev_iter<T: Clone>(list: &[T]) -> Vec<T> {
    list.iter().rev().cloned().collect()
}

/// Tail-recursive Fibonacci with accumulator.
pub fn fib_tr(n: u64) -> u64 {
    fn go(a: u64, b: u64, n: u64) -> u64 {
        match n {
            0 => a,
            n => go(b, a + b, n - 1),
        }
    }
    go(0, 1, n)
}

/// Iterative Fibonacci โ€” idiomatic Rust.
pub fn fib_iter(n: u64) -> u64 {
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 0..n {
        let tmp = a + b;
        a = b;
        b = tmp;
    }
    a
}

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

    #[test]
    fn test_sum() {
        let big: Vec<i64> = (1..=100_000).collect();
        let expected: i64 = 100_000 * 100_001 / 2;
        assert_eq!(sum_tr(&big), expected);
        assert_eq!(sum_iter(&big), expected);
    }

    #[test]
    fn test_sum_empty() {
        assert_eq!(sum_tr(&[]), 0);
        assert_eq!(sum_iter(&[]), 0);
    }

    #[test]
    fn test_rev() {
        assert_eq!(rev_tr(&[1, 2, 3]), vec![3, 2, 1]);
        assert_eq!(rev_recursive(&[1, 2, 3]), vec![3, 2, 1]);
        assert_eq!(rev_tr::<i32>(&[]), Vec::<i32>::new());
    }

    #[test]
    fn test_rev_iter() {
        assert_eq!(rev_iter(&[1, 2, 3]), vec![3, 2, 1]);
        assert_eq!(rev_iter::<i32>(&[]), Vec::<i32>::new());
    }

    #[test]
    fn test_fib() {
        assert_eq!(fib_tr(0), 0);
        assert_eq!(fib_tr(1), 1);
        assert_eq!(fib_tr(10), 55);
        assert_eq!(fib_tr(40), 102334155);
    }

    #[test]
    fn test_fib_iter() {
        assert_eq!(fib_iter(10), 55);
        assert_eq!(fib_iter(40), 102334155);
    }
}
let rec sum_naive = function
  | []     -> 0
  | h :: t -> h + sum_naive t

let sum_tr lst =
  let rec go acc = function
    | []     -> acc
    | h :: t -> go (acc + h) t
  in go 0 lst

let rev_tr lst =
  let rec go acc = function
    | []     -> acc
    | h :: t -> go (h :: acc) t
  in go [] lst

let fib_tr n =
  let rec go a b = function
    | 0 -> a
    | n -> go b (a + b) (n - 1)
  in go 0 1 n

let () =
  let big = List.init 100_000 (fun i -> i + 1) in
  assert (sum_tr big = 5000050000);
  assert (List.hd (rev_tr big) = 100000);
  assert (fib_tr 10 = 55);
  assert (fib_tr 40 = 102334155);
  print_endline "All assertions passed."

๐Ÿ“Š Detailed Comparison

Tail-Recursive Accumulator Pattern: OCaml vs Rust

The Core Insight

This pattern reveals a fundamental philosophical difference: OCaml optimizes recursion (via TCO) to make it as efficient as loops; Rust provides powerful iterators that make loops as expressive as recursion. Both achieve stack safety, but through opposite strategies.

OCaml Approach

OCaml guarantees tail-call optimization: if a function's last action is a recursive call, the compiler reuses the current stack frame. The accumulator pattern (`let rec go acc = function ...`) transforms any linear recursion into tail position. This is essential for processing large lists โ€” without it, `sum_naive` on 100K elements would overflow the stack. The pattern is so fundamental that OCaml programmers internalize it early.

Rust Approach

Rust does NOT guarantee TCO (though LLVM sometimes applies it as an optimization). Instead, idiomatic Rust uses iterators: `list.iter().sum()` is stack-safe by design because it compiles to a simple loop. The iterator approach is also more composable โ€” you can chain `.filter()`, `.map()`, `.take()` etc. For Fibonacci, a simple `for` loop with mutable accumulators is clearest. Recursive patterns still work for small inputs or tree structures.

Side-by-Side

ConceptOCamlRust
Stack safetyGuaranteed TCOIterators / loops
Sum`let rec go acc = function ...``iter().sum()`
Reverse`go (h :: acc) t``iter().rev().collect()`
Fibonacci`go a b (n-1)``for` loop with mutation
Large inputTCO handles itIterators handle it
StyleRecursion is idiomaticIteration is idiomatic

What Rust Learners Should Notice

  • Don't write recursive functions in Rust for linear data โ€” use iterators. They're zero-cost abstractions that compile to the same machine code as hand-written loops
  • The accumulator pattern is still useful for tree structures where iterators don't naturally apply
  • Rust's slice patterns (`[h, rest @ ..]`) are powerful for recursive code but less common than iterator chains
  • `iter().sum()` requires the `Sum` trait โ€” Rust's trait system handles the dispatch
  • When you see OCaml's tail recursion, think "what iterator chain would express this in Rust?"

Further Reading

  • [The Rust Book โ€” Iterators](https://doc.rust-lang.org/book/ch13-02-iterators.html)
  • [Cornell CS3110 โ€” Tail Recursion](https://cs3110.github.io/textbook/chapters/data/lists.html)