ExamplesBy LevelBy TopicLearning Paths
115 Intermediate

115-vec-patterns — Vec Iterator Patterns

Functional Programming

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

  • • Chain filter, map, and sum in a single lazy pass
  • • Use zip to pair two iterators element-wise
  • • Use flat_map to expand-and-flatten (OCaml's List.concat_map)
  • • Build a histogram with fold
  • • Compute prefix sums with scan
  • Code 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 time

    Key Differences

  • Laziness: Rust's iterator chain is lazy (no intermediate collections); OCaml's 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![]);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    List type'a list&[T] (slice)
    MapList.map : ('a -> 'b) -> 'a list -> 'b list.map(f) on Iterator
    FilterList.filter : ('a -> bool) -> 'a list -> 'a list.filter(pred) on Iterator
    FoldList.fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a.fold(init, f) on Iterator
    ZipList.map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list.zip() on Iterator
    PartitionList.partition : ('a -> bool) -> 'a list -> 'a list * 'a list.partition(pred) on Iterator
    Flat-mapList.concat_map : ('a -> 'b list) -> 'a list -> 'b list.flat_map(f) on Iterator

    Key Insights

  • Laziness: OCaml's 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.
  • Allocation: Chaining three OCaml list operations allocates three separate lists. The equivalent Rust iterator chain allocates exactly once (at .collect()) — or zero times if the terminal is .sum(), .fold(), or similar.
  • Ownership and borrowing: Rust's 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.
  • Double-reference pattern: 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 vs fold: Rust's .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

  • Write word_count_pipeline(text: &str) -> HashMap<String, usize> using split_whitespace().map().fold() in a single chain.
  • Implement sliding_window_averages(data: &[f64], k: usize) -> Vec<f64> using windows(k) and map.
  • Write group_consecutive_equal(data: &[i32]) -> Vec<Vec<i32>> using fold to collect equal consecutive elements into groups.
  • Open Source Repos