906-iterator-windows — Iterator Windows
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
.windows(k) to produce overlapping zero-copy sub-slice referenceswindows(k).map(|w| sum/k)windows(2).all(|w| w[0] < w[1])windows(3).filter(|w| w[1] > w[0] && w[1] > w[2])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
.windows(n) yields references into the original slice — zero allocation per window; OCaml Array.sub allocates a new array per window.n elements; OCaml Array.sub can raise Invalid_argument on incorrect bounds.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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Windows function | val windows : int -> 'a array -> 'a array array | fn windows(&[T], usize) -> impl Iterator<Item=&[T]> |
| Each window | 'a array (heap-allocated copy) | &[T] (borrowed sub-slice, zero-copy) |
| Result collection | Array.map returning 'a array | .collect::<Vec<_>>() |
| Memory model | Allocates L - n + 1 new arrays | Single allocation for the original slice |
Key Insights
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.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.windows() returns an Iterator, it chains directly with .enumerate(), .filter(), .map(), .all(), .any() — no intermediate collections needed until .collect() at the very end.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.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
max_subarray_sum(data: &[i32], k: usize) -> i32 that finds the maximum sum over all windows of size k.trend_labels(prices: &[f64]) -> Vec<&str> using windows(2) that labels each transition as "up", "down", or "flat".autocorrelation(data: &[f64], lag: usize) -> f64 using two windows offset by lag to compute the correlation.