115-vec-patterns — Vec Iterator Patterns
Tutorial
The Problem
The vector iterator pattern — filter, map, fold, zip, flat_map, scan — is the functional programming toolkit applied to Rust's Vec<T>. These adapters are lazy, composable, and zero-overhead: the compiler fuses them into a single loop with no intermediate allocations.
This example demonstrates the full map-filter-fold pipeline, showing how OCaml's List.map, List.filter, List.fold_left, and List.concat_map translate to Rust's iterator chain.
🎯 Learning Outcomes
filter, map, and sum in a single lazy passzip to pair two iterators element-wiseflat_map to expand-and-flatten (OCaml's List.concat_map)foldscanCode Example
pub fn sum_of_doubled_evens(data: &[i32]) -> i32 {
data.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.sum()
}
// No intermediate Vec — elements flow through the pipeline one at a timeKey Differences
List.map |> List.filter creates intermediate lists at each step.partition**: Rust's Iterator::partition produces two Vecs in one pass; OCaml's List.partition is also one pass.flat_map vs concat_map**: Rust's flat_map(|x| 0..x) flattens ranges; OCaml uses List.concat_map which flattens lists.scan vs running fold**: Rust's scan is a first-class iterator adapter; OCaml has no direct equivalent in stdlib (use List.fold_left with accumulation).OCaml Approach
(* sum_of_doubled_evens *)
let sum_doubled_evens data =
data
|> List.filter (fun x -> x mod 2 = 0)
|> List.map (fun x -> x * 2)
|> List.fold_left (+) 0
(* flat_map *)
let expand_range data =
List.concat_map (fun x -> List.init x Fun.id) data
OCaml's |> pipeline is strict — each step allocates a new list. Rust's .filter().map().sum() chain is lazy and allocation-free until .collect().
Full Source
#![allow(clippy::all)]
/// Approach 1: map, filter, fold via iterator adapters (lazy, zero intermediate allocation)
/// OCaml: List.filter f |> List.map g |> List.fold_left (+) 0
pub fn sum_of_doubled_evens(data: &[i32]) -> i32 {
data.iter().filter(|&&x| x % 2 == 0).map(|&x| x * 2).sum()
}
/// Approach 2: zip pairs of slices into tuples
/// OCaml: List.map2 (fun x y -> (x, y)) a b
pub fn zip_names_ages<'a>(names: &[&'a str], ages: &[u32]) -> Vec<(&'a str, u32)> {
names.iter().copied().zip(ages.iter().copied()).collect()
}
type PartitionedPairs<'a> = (Vec<(&'a str, u32)>, Vec<(&'a str, u32)>);
/// Partition pairs by age threshold — OCaml: List.partition (fun (_, age) -> age < threshold)
pub fn partition_by_age<'a>(pairs: &[(&'a str, u32)], threshold: u32) -> PartitionedPairs<'a> {
pairs.iter().copied().partition(|&(_, age)| age < threshold)
}
/// Approach 3: flat_map (OCaml: List.concat_map / List.flatten ∘ List.map)
/// Expands each element into multiple elements and flattens the result.
pub fn expand_range(data: &[i32]) -> Vec<i32> {
data.iter().flat_map(|&x| 0..x).collect()
}
/// Approach 4: fold to build a HashMap histogram
/// OCaml: List.fold_left (fun acc x -> ...) empty_map data
pub fn histogram(data: &[i32]) -> std::collections::HashMap<i32, usize> {
data.iter()
.fold(std::collections::HashMap::new(), |mut acc, &x| {
*acc.entry(x).or_insert(0) += 1;
acc
})
}
/// Approach 5: scan — running totals (prefix sums)
/// OCaml: no direct equivalent; closest is a custom fold that keeps intermediates
pub fn prefix_sums(data: &[i32]) -> Vec<i32> {
data.iter()
.scan(0_i32, |acc, &x| {
*acc += x;
Some(*acc)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_of_doubled_evens_basic() {
let data: Vec<i32> = (1..=10).collect();
assert_eq!(sum_of_doubled_evens(&data), 60);
}
#[test]
fn test_sum_of_doubled_evens_empty() {
assert_eq!(sum_of_doubled_evens(&[]), 0);
}
#[test]
fn test_sum_of_doubled_evens_all_odd() {
assert_eq!(sum_of_doubled_evens(&[1, 3, 5]), 0);
}
#[test]
fn test_zip_names_ages() {
let names = ["Alice", "Bob", "Charlie"];
let ages = [30_u32, 25, 35];
let pairs = zip_names_ages(&names, &ages);
assert_eq!(pairs, vec![("Alice", 30), ("Bob", 25), ("Charlie", 35)]);
}
#[test]
fn test_partition_by_age() {
let names = ["Alice", "Bob", "Charlie"];
let ages = [30_u32, 25, 35];
let pairs = zip_names_ages(&names, &ages);
let (young, old) = partition_by_age(&pairs, 30);
assert_eq!(young.len(), 1);
assert_eq!(old.len(), 2);
assert_eq!(young[0].0, "Bob");
}
#[test]
fn test_expand_range() {
// flat_map: [1,3] → [0] ++ [0,1,2] = [0,0,1,2]
assert_eq!(expand_range(&[1, 3]), vec![0, 0, 1, 2]);
assert_eq!(expand_range(&[]), vec![]);
}
#[test]
fn test_histogram() {
let hist = histogram(&[1, 2, 1, 3, 2, 1]);
assert_eq!(hist[&1], 3);
assert_eq!(hist[&2], 2);
assert_eq!(hist[&3], 1);
}
#[test]
fn test_prefix_sums() {
assert_eq!(prefix_sums(&[1, 2, 3, 4]), vec![1, 3, 6, 10]);
assert_eq!(prefix_sums(&[]), vec![]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_of_doubled_evens_basic() {
let data: Vec<i32> = (1..=10).collect();
assert_eq!(sum_of_doubled_evens(&data), 60);
}
#[test]
fn test_sum_of_doubled_evens_empty() {
assert_eq!(sum_of_doubled_evens(&[]), 0);
}
#[test]
fn test_sum_of_doubled_evens_all_odd() {
assert_eq!(sum_of_doubled_evens(&[1, 3, 5]), 0);
}
#[test]
fn test_zip_names_ages() {
let names = ["Alice", "Bob", "Charlie"];
let ages = [30_u32, 25, 35];
let pairs = zip_names_ages(&names, &ages);
assert_eq!(pairs, vec![("Alice", 30), ("Bob", 25), ("Charlie", 35)]);
}
#[test]
fn test_partition_by_age() {
let names = ["Alice", "Bob", "Charlie"];
let ages = [30_u32, 25, 35];
let pairs = zip_names_ages(&names, &ages);
let (young, old) = partition_by_age(&pairs, 30);
assert_eq!(young.len(), 1);
assert_eq!(old.len(), 2);
assert_eq!(young[0].0, "Bob");
}
#[test]
fn test_expand_range() {
// flat_map: [1,3] → [0] ++ [0,1,2] = [0,0,1,2]
assert_eq!(expand_range(&[1, 3]), vec![0, 0, 1, 2]);
assert_eq!(expand_range(&[]), vec![]);
}
#[test]
fn test_histogram() {
let hist = histogram(&[1, 2, 1, 3, 2, 1]);
assert_eq!(hist[&1], 3);
assert_eq!(hist[&2], 2);
assert_eq!(hist[&3], 1);
}
#[test]
fn test_prefix_sums() {
assert_eq!(prefix_sums(&[1, 2, 3, 4]), vec![1, 3, 6, 10]);
assert_eq!(prefix_sums(&[]), vec![]);
}
}
Deep Comparison
OCaml vs Rust: Vec Operations Functionally
Side-by-Side Code
OCaml
let data = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
let sum =
data
|> List.filter (fun x -> x mod 2 = 0)
|> List.map (fun x -> x * 2)
|> List.fold_left ( + ) 0
(* Each step produces a new intermediate list *)
Rust (idiomatic — lazy iterator pipeline)
pub fn sum_of_doubled_evens(data: &[i32]) -> i32 {
data.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.sum()
}
// No intermediate Vec — elements flow through the pipeline one at a time
Rust (functional/recursive — mirrors OCaml explicit recursion)
pub fn sum_of_doubled_evens_rec(data: &[i32]) -> i32 {
match data {
[] => 0,
[x, rest @ ..] if x % 2 == 0 => x * 2 + sum_of_doubled_evens_rec(rest),
[_, rest @ ..] => sum_of_doubled_evens_rec(rest),
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| List type | 'a list | &[T] (slice) |
| Map | List.map : ('a -> 'b) -> 'a list -> 'b list | .map(f) on Iterator |
| Filter | List.filter : ('a -> bool) -> 'a list -> 'a list | .filter(pred) on Iterator |
| Fold | List.fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a | .fold(init, f) on Iterator |
| Zip | List.map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list | .zip() on Iterator |
| Partition | List.partition : ('a -> bool) -> 'a list -> 'a list * 'a list | .partition(pred) on Iterator |
| Flat-map | List.concat_map : ('a -> 'b list) -> 'a list -> 'b list | .flat_map(f) on Iterator |
Key Insights
List.map is strict — it immediately allocates a new list for each step. Rust's .map() is lazy; elements are processed on demand, one at a time, with no intermediate Vec until .collect() is called..collect()) — or zero times if the terminal is .sum(), .fold(), or similar.iter() yields &T references into the original slice, ensuring the source data isn't moved or copied. OCaml's garbage-collected lists share structure implicitly; Rust makes sharing explicit via lifetimes.filter(|&&x| ...) uses two & dereferences because iter() yields &&i32 when the slice element is itself i32 — a common Rust iterator idiom absent from OCaml..scan() is an iterator adapter that yields every intermediate accumulator, making prefix sums trivial. OCaml requires a manual fold_left that threads a growing list through the accumulator.When to Use Each Style
Use idiomatic Rust iterator chains when: you want zero intermediate allocations, readable data-pipeline style code, and maximum performance — the common case for most transformations.
Use recursive Rust when: you need to mirror OCaml's structural recursion for pedagogical clarity, or when the problem decomposes naturally on the head/tail structure of a slice with slice patterns.
Exercises
word_count_pipeline(text: &str) -> HashMap<String, usize> using split_whitespace().map().fold() in a single chain.sliding_window_averages(data: &[f64], k: usize) -> Vec<f64> using windows(k) and map.group_consecutive_equal(data: &[i32]) -> Vec<Vec<i32>> using fold to collect equal consecutive elements into groups.