882-iterator-consumers — Iterator Consumers
Tutorial
The Problem
Iterator adapters transform sequences lazily, but something must drive the evaluation to completion. Iterator consumers "pull" all values out: .collect() materializes into a container, .sum() accumulates into a number, .find() short-circuits at the first match, and .fold() generalizes all of these. OCaml's equivalent consumers are List.fold_left, List.find, List.for_all, List.exists, List.length. Understanding the consumer landscape determines which one to reach for: .fold() is the universal but verbose fallback; specific consumers like .max(), .count(), and .any() communicate intent clearly.
🎯 Learning Outcomes
.fold() as the universal consumer for custom accumulation.sum(), .product(), .max(), .min() are more expressive than fold.find() and .position() for search with early termination.any() and .all() for short-circuiting predicates.fold() into a HashMapCode Example
let sum: i32 = data.iter().sum();
let product: i32 = data.iter().product();
// Or explicit fold:
let sum = data.iter().fold(0, |acc, &x| acc + x);Key Differences
.find() returns Option<T> (never panics); OCaml List.find raises Not_found (use find_opt for safety)..sum(), .product(), .count(), .min(), .max() as first-class consumers; OCaml expresses all via fold_left..entry().or_insert() pattern avoids double-lookup; OCaml's Hashtbl requires separate find + replace..any(), .all(), .find() short-circuit; OCaml's equivalents List.for_all, List.exists, List.find also short-circuit.OCaml Approach
OCaml's List.fold_left f init xs is the primary consumer. List.find pred xs finds the first matching element (raising Not_found rather than returning option — List.find_opt is the safe version). List.for_all and List.exists are the all/any equivalents. Frequency maps use Hashtbl: Hashtbl.replace tbl k (1 + try Hashtbl.find tbl k with Not_found -> 0). OCaml lacks List.sum — it's expressed as List.fold_left (+) 0.
Full Source
#![allow(clippy::all)]
// Example 088: Iterator Consumers
// fold, collect, sum, max, find, position
use std::collections::HashMap;
// === Approach 1: fold — the universal consumer ===
fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
fn product(data: &[i32]) -> i32 {
data.iter().product()
}
fn concat_strs(data: &[&str]) -> String {
data.iter().copied().collect()
}
// fold for custom accumulation
fn running_average(data: &[f64]) -> f64 {
let (sum, count) = data.iter().fold((0.0, 0usize), |(s, c), &x| (s + x, c + 1));
if count == 0 {
0.0
} else {
sum / count as f64
}
}
// === Approach 2: Specific consumers ===
fn find_first(data: &[i32], pred: impl Fn(&i32) -> bool) -> Option<&i32> {
data.iter().find(|x| pred(x))
}
fn find_position(data: &[i32], pred: impl Fn(&i32) -> bool) -> Option<usize> {
data.iter().position(|x| pred(x))
}
fn max_of(data: &[i32]) -> Option<&i32> {
data.iter().max()
}
fn min_of(data: &[i32]) -> Option<&i32> {
data.iter().min()
}
fn count_matching(data: &[i32], pred: impl Fn(&i32) -> bool) -> usize {
data.iter().filter(|x| pred(x)).count()
}
fn any_match(data: &[i32], pred: impl Fn(&i32) -> bool) -> bool {
data.iter().any(|x| pred(&x))
}
fn all_match(data: &[i32], pred: impl Fn(&i32) -> bool) -> bool {
data.iter().all(|x| pred(&x))
}
// === Approach 3: Complex consumers ===
fn frequencies(data: &[i32]) -> HashMap<i32, usize> {
let mut map = HashMap::new();
for &x in data {
*map.entry(x).or_insert(0) += 1;
}
map
}
// Equivalent using fold
fn frequencies_fold(data: &[i32]) -> HashMap<i32, usize> {
data.iter().fold(HashMap::new(), |mut acc, &x| {
*acc.entry(x).or_insert(0) += 1;
acc
})
}
fn group_by<T, K: std::hash::Hash + Eq>(data: &[T], key: impl Fn(&T) -> K) -> HashMap<K, Vec<&T>> {
let mut map: HashMap<K, Vec<&T>> = HashMap::new();
for item in data {
map.entry(key(item)).or_default().push(item);
}
map
}
// Collect into different types
fn collect_examples() {
let data = vec![1, 2, 3, 4, 5];
let _vec: Vec<i32> = data.iter().copied().collect();
let _set: std::collections::HashSet<i32> = data.iter().copied().collect();
let _string: String = data
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ");
// Collect Result<T, E> — stops at first error
let results: Vec<Result<i32, _>> = vec![Ok(1), Ok(2), Ok(3)];
let _all: Result<Vec<i32>, String> = results.into_iter().collect();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum(&[1, 2, 3, 4, 5]), 15);
}
#[test]
fn test_product() {
assert_eq!(product(&[1, 2, 3, 4, 5]), 120);
}
#[test]
fn test_concat() {
assert_eq!(concat_strs(&["a", "b", "c"]), "abc");
}
#[test]
fn test_average() {
assert!((running_average(&[1.0, 2.0, 3.0]) - 2.0).abs() < 1e-10);
assert_eq!(running_average(&[]), 0.0);
}
#[test]
fn test_find() {
assert_eq!(find_first(&[1, 2, 3, 4, 5], |x| *x > 3), Some(&4));
assert_eq!(find_first(&[1, 2, 3], |x| *x > 10), None);
}
#[test]
fn test_position() {
assert_eq!(find_position(&[1, 2, 3, 4, 5], |x| *x > 3), Some(3));
assert_eq!(find_position(&[1, 2, 3], |x| *x > 10), None);
}
#[test]
fn test_max_min() {
assert_eq!(max_of(&[3, 1, 4, 1, 5, 9]), Some(&9));
assert_eq!(min_of(&[3, 1, 4, 1, 5, 9]), Some(&1));
assert_eq!(max_of(&[]), None);
}
#[test]
fn test_count() {
assert_eq!(count_matching(&[1, 2, 3, 4, 5, 6], |x| x % 2 == 0), 3);
}
#[test]
fn test_any_all() {
assert!(any_match(&[1, 2, 3, 4, 5, 6], |x| *x > 5));
assert!(all_match(&[1, 2, 3], |x| *x > 0));
assert!(!all_match(&[1, -2, 3], |x| *x > 0));
}
#[test]
fn test_frequencies() {
let f = frequencies(&[1, 2, 1, 3, 2, 1]);
assert_eq!(f[&1], 3);
assert_eq!(f[&2], 2);
assert_eq!(f[&3], 1);
}
#[test]
fn test_frequencies_fold() {
let f = frequencies_fold(&[1, 2, 1, 3, 2, 1]);
assert_eq!(f[&1], 3);
}
#[test]
fn test_group_by() {
let words = vec!["hello", "hi", "world", "wow"];
let groups = group_by(&words, |w| w.chars().next().unwrap());
assert_eq!(groups[&'h'].len(), 2);
assert_eq!(groups[&'w'].len(), 2);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum(&[1, 2, 3, 4, 5]), 15);
}
#[test]
fn test_product() {
assert_eq!(product(&[1, 2, 3, 4, 5]), 120);
}
#[test]
fn test_concat() {
assert_eq!(concat_strs(&["a", "b", "c"]), "abc");
}
#[test]
fn test_average() {
assert!((running_average(&[1.0, 2.0, 3.0]) - 2.0).abs() < 1e-10);
assert_eq!(running_average(&[]), 0.0);
}
#[test]
fn test_find() {
assert_eq!(find_first(&[1, 2, 3, 4, 5], |x| *x > 3), Some(&4));
assert_eq!(find_first(&[1, 2, 3], |x| *x > 10), None);
}
#[test]
fn test_position() {
assert_eq!(find_position(&[1, 2, 3, 4, 5], |x| *x > 3), Some(3));
assert_eq!(find_position(&[1, 2, 3], |x| *x > 10), None);
}
#[test]
fn test_max_min() {
assert_eq!(max_of(&[3, 1, 4, 1, 5, 9]), Some(&9));
assert_eq!(min_of(&[3, 1, 4, 1, 5, 9]), Some(&1));
assert_eq!(max_of(&[]), None);
}
#[test]
fn test_count() {
assert_eq!(count_matching(&[1, 2, 3, 4, 5, 6], |x| x % 2 == 0), 3);
}
#[test]
fn test_any_all() {
assert!(any_match(&[1, 2, 3, 4, 5, 6], |x| *x > 5));
assert!(all_match(&[1, 2, 3], |x| *x > 0));
assert!(!all_match(&[1, -2, 3], |x| *x > 0));
}
#[test]
fn test_frequencies() {
let f = frequencies(&[1, 2, 1, 3, 2, 1]);
assert_eq!(f[&1], 3);
assert_eq!(f[&2], 2);
assert_eq!(f[&3], 1);
}
#[test]
fn test_frequencies_fold() {
let f = frequencies_fold(&[1, 2, 1, 3, 2, 1]);
assert_eq!(f[&1], 3);
}
#[test]
fn test_group_by() {
let words = vec!["hello", "hi", "world", "wow"];
let groups = group_by(&words, |w| w.chars().next().unwrap());
assert_eq!(groups[&'h'].len(), 2);
assert_eq!(groups[&'w'].len(), 2);
}
}
Deep Comparison
Comparison: Iterator Consumers
Fold
OCaml:
let sum lst = List.fold_left (+) 0 lst
let product lst = List.fold_left ( * ) 1 lst
Rust:
let sum: i32 = data.iter().sum();
let product: i32 = data.iter().product();
// Or explicit fold:
let sum = data.iter().fold(0, |acc, &x| acc + x);
Find / Position
OCaml:
let find_first pred lst =
try Some (List.find pred lst) with Not_found -> None
let rec find_position pred i = function
| [] -> None
| x :: _ when pred x -> Some i
| _ :: rest -> find_position pred (i+1) rest
Rust:
data.iter().find(|&&x| x > 3) // Option<&&i32>
data.iter().position(|&x| x > 3) // Option<usize>
Frequencies
OCaml:
let frequencies lst =
let tbl = Hashtbl.create 16 in
List.iter (fun x ->
let c = try Hashtbl.find tbl x with Not_found -> 0 in
Hashtbl.replace tbl x (c + 1)
) lst;
Hashtbl.fold (fun k v acc -> (k, v) :: acc) tbl []
Rust:
fn frequencies(data: &[i32]) -> HashMap<i32, usize> {
data.iter().fold(HashMap::new(), |mut acc, &x| {
*acc.entry(x).or_insert(0) += 1;
acc
})
}
Exercises
.fold() to implement max_by_key<T, K: Ord, F: Fn(&T) -> K> without using .max_by_key().word_count_fold that builds a HashMap<String, usize> using a single .fold() call over tokenized words.partition_by using a single .fold() that splits a slice into two Vecs based on a predicate.