ExamplesBy LevelBy TopicLearning Paths
906 Intermediate

906-iterator-windows — Iterator Windows

Functional Programming

Tutorial

The Problem

Signal processing, financial time-series analysis, and machine learning feature engineering all use sliding window operations: compute a statistic over each overlapping sub-window of a sequence. A window of size k over n elements produces n-k+1 windows. Naive implementation requires O(k) work per window; efficient windowed algorithms precompute partial results. Rust's .windows(n) on slices yields zero-copy overlapping sub-slices in O(1) per window, enabling cache-friendly windowed computation. OCaml lacks a built-in equivalent and requires manual implementation.

🎯 Learning Outcomes

  • • Use .windows(k) to produce overlapping zero-copy sub-slice references
  • • Compute moving averages using windows(k).map(|w| sum/k)
  • • Detect monotonic subsequences using windows(2).all(|w| w[0] < w[1])
  • • Find local maxima using windows(3).filter(|w| w[1] > w[0] && w[1] > w[2])
  • • Extract bigrams (consecutive pairs) using windows(2).map(|w| (&w[0], &w[1]))
  • Code Example

    // Zero-copy: windows() yields &[T] references into the original slice
    pub fn moving_average(data: &[i32], k: usize) -> Vec<f64> {
        data.windows(k)
            .map(|w| w.iter().sum::<i32>() as f64 / k as f64)
            .collect()
    }

    Key Differences

  • Zero-copy: Rust .windows(n) yields references into the original slice — zero allocation per window; OCaml Array.sub allocates a new array per window.
  • Bounds guarantee: Rust windows always have exactly n elements; OCaml Array.sub can raise Invalid_argument on incorrect bounds.
  • List vs slice: OCaml lists cannot provide O(1) windows; Rust slice windows are O(1) due to contiguous memory layout.
  • Enumerate + index: Rust can combine windows with enumerate to get the window's position; OCaml's functional approach loses positional information.
  • OCaml Approach

    OCaml arrays support Array.sub arr i k for windowed access: Array.init (n - k + 1) (fun i -> f (Array.sub arr i k)). For lists, recursion is required: let rec windows k = function | lst when List.length lst < k -> [] | lst -> List.filteri (fun i _ -> i < k) lst :: windows k (List.tl lst). This is O(n²) for lists. The Bigarray module provides stride-based views for numerical code.

    Full Source

    #![allow(clippy::all)]
    //! 262. Sliding Windows over Slices
    //!
    //! `windows(n)` yields overlapping sub-slices of length `n`, zero-copy.
    //! Each window shares memory with the original slice — no allocation at each step.
    
    /// Compute moving averages over a slice using a window of size `k`.
    ///
    /// Returns one average per window. Slice borrows the original data — no copies.
    pub fn moving_average(data: &[i32], k: usize) -> Vec<f64> {
        data.windows(k)
            .map(|w| w.iter().sum::<i32>() as f64 / k as f64)
            .collect()
    }
    
    /// Return true if every consecutive pair is strictly increasing.
    pub fn is_strictly_increasing(data: &[i32]) -> bool {
        data.windows(2).all(|w| w[0] < w[1])
    }
    
    /// Find indices of local maxima: elements strictly greater than both neighbours.
    ///
    /// Returns the index into the *original* slice (window index + 1).
    pub fn local_maxima(data: &[i32]) -> Vec<usize> {
        data.windows(3)
            .enumerate()
            .filter(|(_, w)| w[1] > w[0] && w[1] > w[2])
            .map(|(i, _)| i + 1) // +1: window index is the left neighbour's index
            .collect()
    }
    
    /// Extract all bigrams (consecutive pairs) from a slice of items.
    pub fn bigrams<T>(data: &[T]) -> Vec<(&T, &T)> {
        data.windows(2).map(|w| (&w[0], &w[1])).collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_moving_average_basic() {
            let data = [1, 2, 3, 4, 5];
            let avgs = moving_average(&data, 3);
            assert_eq!(avgs, vec![2.0, 3.0, 4.0]);
        }
    
        #[test]
        fn test_moving_average_window_equals_len() {
            let data = [1, 2, 3];
            let avgs = moving_average(&data, 3);
            assert_eq!(avgs, vec![2.0]);
        }
    
        #[test]
        fn test_moving_average_window_larger_than_slice() {
            let data = [1, 2];
            let avgs = moving_average(&data, 5);
            assert!(avgs.is_empty());
        }
    
        #[test]
        fn test_is_strictly_increasing_true() {
            assert!(is_strictly_increasing(&[1, 2, 3, 4, 5]));
        }
    
        #[test]
        fn test_is_strictly_increasing_false() {
            assert!(!is_strictly_increasing(&[1, 2, 2, 3]));
            assert!(!is_strictly_increasing(&[5, 4, 3]));
        }
    
        #[test]
        fn test_is_strictly_increasing_single() {
            // A single-element slice has no pairs → vacuously true
            assert!(is_strictly_increasing(&[42]));
        }
    
        #[test]
        fn test_local_maxima() {
            let signal = [1, 3, 2, 5, 4, 6, 2];
            let peaks = local_maxima(&signal);
            // index 1 (value 3): 1 < 3 > 2 ✓
            // index 3 (value 5): 2 < 5 > 4 ✓
            // index 5 (value 6): 4 < 6 > 2 ✓
            assert_eq!(peaks, vec![1, 3, 5]);
        }
    
        #[test]
        fn test_local_maxima_flat() {
            let signal = [1, 1, 1, 1];
            assert!(local_maxima(&signal).is_empty());
        }
    
        #[test]
        fn test_bigrams() {
            let words = ["the", "cat", "sat"];
            let pairs = bigrams(&words);
            assert_eq!(pairs, vec![(&"the", &"cat"), (&"cat", &"sat")]);
        }
    
        #[test]
        fn test_bigrams_empty() {
            let empty: &[i32] = &[];
            assert!(bigrams(empty).is_empty());
        }
    
        #[test]
        fn test_window_count() {
            // L=5, n=3 → L - n + 1 = 3 windows
            let data = [10, 20, 30, 40, 50];
            assert_eq!(data.windows(3).count(), 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_moving_average_basic() {
            let data = [1, 2, 3, 4, 5];
            let avgs = moving_average(&data, 3);
            assert_eq!(avgs, vec![2.0, 3.0, 4.0]);
        }
    
        #[test]
        fn test_moving_average_window_equals_len() {
            let data = [1, 2, 3];
            let avgs = moving_average(&data, 3);
            assert_eq!(avgs, vec![2.0]);
        }
    
        #[test]
        fn test_moving_average_window_larger_than_slice() {
            let data = [1, 2];
            let avgs = moving_average(&data, 5);
            assert!(avgs.is_empty());
        }
    
        #[test]
        fn test_is_strictly_increasing_true() {
            assert!(is_strictly_increasing(&[1, 2, 3, 4, 5]));
        }
    
        #[test]
        fn test_is_strictly_increasing_false() {
            assert!(!is_strictly_increasing(&[1, 2, 2, 3]));
            assert!(!is_strictly_increasing(&[5, 4, 3]));
        }
    
        #[test]
        fn test_is_strictly_increasing_single() {
            // A single-element slice has no pairs → vacuously true
            assert!(is_strictly_increasing(&[42]));
        }
    
        #[test]
        fn test_local_maxima() {
            let signal = [1, 3, 2, 5, 4, 6, 2];
            let peaks = local_maxima(&signal);
            // index 1 (value 3): 1 < 3 > 2 ✓
            // index 3 (value 5): 2 < 5 > 4 ✓
            // index 5 (value 6): 4 < 6 > 2 ✓
            assert_eq!(peaks, vec![1, 3, 5]);
        }
    
        #[test]
        fn test_local_maxima_flat() {
            let signal = [1, 1, 1, 1];
            assert!(local_maxima(&signal).is_empty());
        }
    
        #[test]
        fn test_bigrams() {
            let words = ["the", "cat", "sat"];
            let pairs = bigrams(&words);
            assert_eq!(pairs, vec![(&"the", &"cat"), (&"cat", &"sat")]);
        }
    
        #[test]
        fn test_bigrams_empty() {
            let empty: &[i32] = &[];
            assert!(bigrams(empty).is_empty());
        }
    
        #[test]
        fn test_window_count() {
            // L=5, n=3 → L - n + 1 = 3 windows
            let data = [10, 20, 30, 40, 50];
            assert_eq!(data.windows(3).count(), 3);
        }
    }

    Deep Comparison

    OCaml vs Rust: Sliding Windows over Slices

    Side-by-Side Code

    OCaml

    (* OCaml has no built-in windows — must allocate a new sub-array each step *)
    let windows n arr =
      let len = Array.length arr in
      if n > len then [||]
      else Array.init (len - n + 1) (fun i -> Array.sub arr i n)
    
    (* Moving average using allocated windows *)
    let moving_average k arr =
      let ws = windows k arr in
      Array.map (fun w ->
        float_of_int (Array.fold_left (+) 0 w) /. float_of_int k
      ) ws
    

    Rust (idiomatic)

    // Zero-copy: windows() yields &[T] references into the original slice
    pub fn moving_average(data: &[i32], k: usize) -> Vec<f64> {
        data.windows(k)
            .map(|w| w.iter().sum::<i32>() as f64 / k as f64)
            .collect()
    }
    

    Rust (functional/chained)

    // Local maxima: combine windows() with enumerate() and filter()
    pub fn local_maxima(data: &[i32]) -> Vec<usize> {
        data.windows(3)
            .enumerate()
            .filter(|(_, w)| w[1] > w[0] && w[1] > w[2])
            .map(|(i, _)| i + 1)
            .collect()
    }
    

    Type Signatures

    ConceptOCamlRust
    Windows functionval windows : int -> 'a array -> 'a array arrayfn windows(&[T], usize) -> impl Iterator<Item=&[T]>
    Each window'a array (heap-allocated copy)&[T] (borrowed sub-slice, zero-copy)
    Result collectionArray.map returning 'a array.collect::<Vec<_>>()
    Memory modelAllocates L - n + 1 new arraysSingle allocation for the original slice

    Key Insights

  • Zero-copy vs allocation: Rust's windows(n) returns &[T] references into the original data — no heap allocation per window. OCaml's Array.sub allocates a fresh array for every window, which means O(L·n) allocations for a slice of length L with window size n.
  • Built-in vs hand-rolled: windows() is a first-class method on Rust slices (&[T]). OCaml's standard library has no equivalent; idiomatic OCaml requires Array.init + Array.sub or a recursive helper, both of which allocate.
  • Iterator composition: Because windows() returns an Iterator, it chains directly with .enumerate(), .filter(), .map(), .all(), .any() — no intermediate collections needed until .collect() at the very end.
  • Bounds safety: Index arithmetic (data[i-1], data[i], data[i+1]) requires manual bounds checking and is a common source of off-by-one bugs. windows(3) guarantees every sub-slice has exactly 3 elements, so w[0], w[1], w[2] are always valid.
  • Window count formula: For a slice of length L and window size n, both languages produce exactly L - n + 1 windows (or zero if n > L). The formula is identical; only the mechanism differs.
  • When to Use Each Style

    **Use windows() iterator chain when:** you need any sliding-window computation — moving averages, monotonicity checks, local extrema, n-gram extraction, or consecutive-pair comparisons. It is the idiomatic, zero-cost abstraction in Rust.

    Use recursive Rust when: you are explicitly demonstrating the structural recursion that mirrors OCaml's pattern-matching style, or when the window logic itself is recursive in nature (e.g., building a tree from overlapping segments).

    Exercises

  • Implement max_subarray_sum(data: &[i32], k: usize) -> i32 that finds the maximum sum over all windows of size k.
  • Write trend_labels(prices: &[f64]) -> Vec<&str> using windows(2) that labels each transition as "up", "down", or "flat".
  • Implement autocorrelation(data: &[f64], lag: usize) -> f64 using two windows offset by lag to compute the correlation.
  • Open Source Repos