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
- Stack-safe computation: understanding tail position lets you convert any recursive algorithm to a safe iterative one โ essential for processing deeply nested structures.
- Mental model for loops: tail-recursive functions and `while` loops are the same computation โ one is declarative, one is imperative.
- Recognizing when OCaml code needs `_tr` variants: OCaml standard library has `List.fold_left` (tail-recursive) and effectively discourages `List.fold_right` for large lists โ now you know why.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| TCO guarantee | Yes โ tail calls are optimized | No โ LLVM may optimize, not guaranteed |
| Accumulator pattern | Same structure; safe due to TCO | Instructive but use iterators for safety |
| `List.rev` | Uses `rev_append` (tail-recursive) | `iter().rev().cloned().collect()` |
| Preferred idiom | Tail-recursive helpers with `go` | `iter().sum()`, `iter().fold()`, explicit loops |
| Stack overflow risk | Eliminated by TCO for tail calls | Still 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
| Concept | OCaml | Rust |
|---|---|---|
| Stack safety | Guaranteed TCO | Iterators / 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 input | TCO handles it | Iterators handle it |
| Style | Recursion is idiomatic | Iteration 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)