104-scan-left — Scan Left (Running Accumulation)
Tutorial Video
Text description (accessibility)
This video demonstrates the "104-scan-left — Scan Left (Running Accumulation)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. `fold` reduces a sequence to one value. Key difference from OCaml: 1. **Laziness**: Rust's `Iterator::scan` is lazy — values are computed only when consumed; OCaml's `List`
Tutorial
The Problem
fold reduces a sequence to one value. scan (also called scanl in Haskell or scan_left in OCaml) returns all intermediate accumulator values — a prefix sum, running maximum, or cumulative product. This is the essential operation for financial running totals, cumulative distribution functions, and online statistics algorithms.
Rust's Iterator::scan implements this lazily, making it composable with the full iterator adapter chain.
🎯 Learning Outcomes
Iterator::scan to produce running totals and running extremesscan_left mirroring OCaml's List.scan_leftscan is fold that emits intermediate resultsscan with other iterator adapters for complex streaming computationsCode Example
xs.iter().scan(init, |state, &x| {
*state = f(*state, x);
Some(*state)
})Key Differences
Iterator::scan is lazy — values are computed only when consumed; OCaml's List-based scan is strict.scan closure receives &mut state for mutation; OCaml's fold passes state by value (immutable by default).running_sum function uses once(0).chain(...) to include it — matching OCaml's scan_left which includes the initial accumulator.scan_left name**: OCaml uses List.scan_left (or fold_left with accumulation); Rust uses Iterator::scan.OCaml Approach
let scan_left f init xs =
List.fold_left (fun (acc, result) x ->
let new_acc = f acc x in
(new_acc, result @ [new_acc])
) (init, [init]) xs
|> snd
Base.List.folding_map provides a cleaner one-pass implementation. OCaml's lazy Seq.scan would be the equivalent of Rust's lazy Iterator::scan.
Full Source
#![allow(clippy::all)]
//! # Scan Left — Running Accumulation
//!
//! `scan` returns all intermediate results of a fold.
//! Rust's `Iterator::scan` method does this lazily.
// ---------------------------------------------------------------------------
// Approach A: Iterator::scan (idiomatic Rust)
// ---------------------------------------------------------------------------
pub fn running_sum(xs: &[i32]) -> Vec<i32> {
let mut acc = 0;
std::iter::once(0)
.chain(xs.iter().map(move |&x| {
acc += x;
acc
}))
.collect()
}
pub fn running_max(xs: &[i32]) -> Vec<i32> {
xs.iter()
.scan(i32::MIN, |state, &x| {
*state = (*state).max(x);
Some(*state)
})
.collect()
}
// ---------------------------------------------------------------------------
// Approach B: Manual scan_left (mirrors OCaml)
// ---------------------------------------------------------------------------
pub fn scan_left<T: Clone, U: Clone>(f: impl Fn(&T, &U) -> T, init: T, xs: &[U]) -> Vec<T> {
let mut result = Vec::with_capacity(xs.len() + 1);
result.push(init.clone());
let mut acc = init;
for x in xs {
acc = f(&acc, x);
result.push(acc.clone());
}
result
}
// ---------------------------------------------------------------------------
// Approach C: Fold-based scan
// ---------------------------------------------------------------------------
pub fn scan_fold<T: Clone>(f: impl Fn(T, T) -> T, init: T, xs: &[T]) -> Vec<T> {
xs.iter().fold(vec![init.clone()], |mut res, x| {
let last = res.last().unwrap().clone();
res.push(f(last, x.clone()));
res
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_running_sum() {
assert_eq!(running_sum(&[1, 2, 3, 4, 5]), vec![0, 1, 3, 6, 10, 15]);
}
#[test]
fn test_running_max() {
assert_eq!(
running_max(&[3, 1, 4, 1, 5, 9, 2, 6]),
vec![3, 3, 4, 4, 5, 9, 9, 9]
);
}
#[test]
fn test_scan_left() {
let result = scan_left(|a: &i32, b: &i32| a + b, 0, &[1, 2, 3, 4, 5]);
assert_eq!(result, vec![0, 1, 3, 6, 10, 15]);
}
#[test]
fn test_scan_empty() {
assert_eq!(scan_left(|a: &i32, b: &i32| a + b, 0, &[]), vec![0]);
}
#[test]
fn test_scan_fold() {
assert_eq!(scan_fold(|a, b| a + b, 0, &[1, 2, 3]), vec![0, 1, 3, 6]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_running_sum() {
assert_eq!(running_sum(&[1, 2, 3, 4, 5]), vec![0, 1, 3, 6, 10, 15]);
}
#[test]
fn test_running_max() {
assert_eq!(
running_max(&[3, 1, 4, 1, 5, 9, 2, 6]),
vec![3, 3, 4, 4, 5, 9, 9, 9]
);
}
#[test]
fn test_scan_left() {
let result = scan_left(|a: &i32, b: &i32| a + b, 0, &[1, 2, 3, 4, 5]);
assert_eq!(result, vec![0, 1, 3, 6, 10, 15]);
}
#[test]
fn test_scan_empty() {
assert_eq!(scan_left(|a: &i32, b: &i32| a + b, 0, &[]), vec![0]);
}
#[test]
fn test_scan_fold() {
assert_eq!(scan_fold(|a, b| a + b, 0, &[1, 2, 3]), vec![0, 1, 3, 6]);
}
}
Deep Comparison
Comparison: Scan Left — OCaml vs Rust
Core Insight
Scan is fold's verbose cousin — it keeps all intermediate results. OCaml lacks a built-in scan_left, so you build it by accumulating into a reversed list during fold_left. Rust provides Iterator::scan in the standard library, with a mutable state parameter — more flexible but slightly different semantics (it doesn't automatically include the initial value).
OCaml
let scan_left f init lst =
let _, result = List.fold_left (fun (acc, res) x ->
let acc' = f acc x in (acc', acc' :: res)
) (init, [init]) lst in
List.rev result
Rust — Iterator::scan
xs.iter().scan(init, |state, &x| {
*state = f(*state, x);
Some(*state)
})
Rust — Manual
pub fn scan_left<T: Clone>(f: impl Fn(&T, &T) -> T, init: T, xs: &[T]) -> Vec<T> {
let mut result = vec![init.clone()];
let mut acc = init;
for x in xs { acc = f(&acc, x); result.push(acc.clone()); }
result
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Built-in | No | Iterator::scan |
| Returns init | Must include manually | Not included by default |
| Laziness | Eager (list) | Lazy (iterator) |
| State | Tuple (acc, result_list) | &mut state parameter |
| Reverse needed | Yes (List.rev) | No (push appends) |
| Result type | 'a list | impl Iterator<Item = T> |
Learner Notes
Iterator::scan quirk**: Rust's scan doesn't include the initial value — you need once(init).chain(scan(...)) if you want it&mut State — you mutate it and return Some(output), which can differ from stateList.rev cost**: OCaml's cons-based building produces reversed results; the final List.rev is O(n)xs.fold(init, f) == xs.scan(init, f).last() — fold is scan without historyExercises
running_product(xs: &[i64]) -> Vec<i64> using scan, stopping accumulation at the first zero.cumulative_average(xs: &[f64]) -> Vec<f64> using scan that tracks both the running sum and count.range_sum(prefix: &[i64], i: usize, j: usize) -> i64 with O(1) query time after O(n) preprocessing.