288: Materializing Iterators with collect()
Tutorial Video
Text description (accessibility)
This video demonstrates the "288: Materializing Iterators with collect()" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Lazy iterators describe computations but produce no output until consumed. Key difference from OCaml: 1. **Unified API**: Rust's `collect()` works for all `FromIterator` types via one method; OCaml requires type
Tutorial
The Problem
Lazy iterators describe computations but produce no output until consumed. The collect() method is the primary way to materialize a lazy iterator pipeline into a concrete data structure. Its power lies in genericity: the same collect() call produces a Vec, HashSet, HashMap, String, BTreeMap, or any other FromIterator-implementing type, depending solely on the type annotation. This makes pipelines maximally composable — the output format is a separate decision from the transformation logic.
🎯 Learning Outcomes
collect() as materializing a lazy iterator into any FromIterator<T> type::<Vec<_>>()) to specify the output collection typeHashSet for deduplication, HashMap from pairs, String from charscollect::<Result<Vec<T>, E>>() as the short-circuit pattern for fallible collectionCode Example
let squares: Vec<u32> = (0..5).map(|x| x * x).collect();
// Type annotation tells collect() what to produceKey Differences
collect() works for all FromIterator types via one method; OCaml requires type-specific conversion functions.collect() is selected by the compiler from type annotations alone — no conditional branching.collect::<Result<Vec<T>, E>>() aggregates results, short-circuiting on first error — OCaml requires explicit fold logic.FromIterator makes any user-defined collection participate in collect() — it is an extension point.OCaml Approach
OCaml does not have a unified collect function. Each collection type has its own conversion function: List.of_seq, Array.of_seq, Hashtbl.of_seq, or String.concat "" for strings:
(* OCaml: different function for each target type *)
let lst = List.of_seq (Seq.map (fun x -> x*x) (Seq.init 5 Fun.id))
let arr = Array.of_seq (Seq.map (fun x -> x*x) (Seq.init 5 Fun.id))
Full Source
#![allow(clippy::all)]
//! # Iterator collect()
//!
//! Materialize a lazy iterator into any `FromIterator<T>` type.
use std::collections::{BTreeMap, HashMap, HashSet, LinkedList};
/// Collect squares into a Vec
pub fn collect_squares(n: u32) -> Vec<u32> {
(0..n).map(|x| x * x).collect()
}
/// Collect unique elements into a HashSet
pub fn unique_elements<T: std::hash::Hash + Eq>(items: Vec<T>) -> HashSet<T> {
items.into_iter().collect()
}
/// Collect into a HashMap from pairs
pub fn pairs_to_map<K, V>(pairs: Vec<(K, V)>) -> HashMap<K, V>
where
K: std::hash::Hash + Eq,
{
pairs.into_iter().collect()
}
/// Collect chars into a String
pub fn chars_to_string(chars: &[char]) -> String {
chars.iter().collect()
}
/// Collect into sorted BTreeMap
pub fn sorted_map<K: Ord, V>(pairs: Vec<(K, V)>) -> BTreeMap<K, V> {
pairs.into_iter().collect()
}
/// Collect Result<T> iterator into Result<Vec<T>>
pub fn parse_all(strs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
strs.iter().map(|s| s.parse::<i32>()).collect()
}
/// Alternative: Collect into LinkedList
pub fn collect_linked_list<T>(items: impl Iterator<Item = T>) -> LinkedList<T> {
items.collect()
}
/// Using turbofish syntax
pub fn turbofish_example() -> Vec<i32> {
(1..=5).collect::<Vec<_>>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_vec() {
let v = collect_squares(5);
assert_eq!(v, vec![0, 1, 4, 9, 16]);
}
#[test]
fn test_collect_hashset_dedup() {
let set = unique_elements(vec![1, 2, 2, 3, 3, 3]);
assert_eq!(set.len(), 3);
}
#[test]
fn test_collect_hashmap() {
let map = pairs_to_map(vec![(0, 0), (1, 1), (2, 4)]);
assert_eq!(map[&2], 4);
}
#[test]
fn test_collect_string() {
let s = chars_to_string(&['a', 'b', 'c']);
assert_eq!(s, "abc");
}
#[test]
fn test_collect_btreemap_sorted() {
let map = sorted_map(vec![(3, "c"), (1, "a"), (2, "b")]);
let keys: Vec<_> = map.keys().collect();
assert_eq!(keys, vec![&1, &2, &3]); // Sorted!
}
#[test]
fn test_collect_result_ok() {
let ok = parse_all(&["1", "2", "3"]);
assert_eq!(ok.unwrap(), vec![1, 2, 3]);
}
#[test]
fn test_collect_result_err() {
let err = parse_all(&["1", "x", "3"]);
assert!(err.is_err());
}
#[test]
fn test_linked_list() {
let ll = collect_linked_list(1..=4);
assert_eq!(ll.len(), 4);
assert_eq!(*ll.front().unwrap(), 1);
}
#[test]
fn test_turbofish() {
let v = turbofish_example();
assert_eq!(v, vec![1, 2, 3, 4, 5]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_vec() {
let v = collect_squares(5);
assert_eq!(v, vec![0, 1, 4, 9, 16]);
}
#[test]
fn test_collect_hashset_dedup() {
let set = unique_elements(vec![1, 2, 2, 3, 3, 3]);
assert_eq!(set.len(), 3);
}
#[test]
fn test_collect_hashmap() {
let map = pairs_to_map(vec![(0, 0), (1, 1), (2, 4)]);
assert_eq!(map[&2], 4);
}
#[test]
fn test_collect_string() {
let s = chars_to_string(&['a', 'b', 'c']);
assert_eq!(s, "abc");
}
#[test]
fn test_collect_btreemap_sorted() {
let map = sorted_map(vec![(3, "c"), (1, "a"), (2, "b")]);
let keys: Vec<_> = map.keys().collect();
assert_eq!(keys, vec![&1, &2, &3]); // Sorted!
}
#[test]
fn test_collect_result_ok() {
let ok = parse_all(&["1", "2", "3"]);
assert_eq!(ok.unwrap(), vec![1, 2, 3]);
}
#[test]
fn test_collect_result_err() {
let err = parse_all(&["1", "x", "3"]);
assert!(err.is_err());
}
#[test]
fn test_linked_list() {
let ll = collect_linked_list(1..=4);
assert_eq!(ll.len(), 4);
assert_eq!(*ll.front().unwrap(), 1);
}
#[test]
fn test_turbofish() {
let v = turbofish_example();
assert_eq!(v, vec![1, 2, 3, 4, 5]);
}
}
Deep Comparison
OCaml vs Rust: collect()
Pattern 1: Collect to List/Vec
OCaml
let nums = List.init 5 (fun i -> i * i)
(* List is the natural collection type *)
Rust
let squares: Vec<u32> = (0..5).map(|x| x * x).collect();
// Type annotation tells collect() what to produce
Pattern 2: Collect to Set
OCaml
module StringSet = Set.Make(String)
let words = ["apple"; "banana"; "apple"; "cherry"]
let set = List.fold_left (fun s w -> StringSet.add w s)
StringSet.empty words
Rust
let words = vec!["apple", "banana", "apple", "cherry"];
let set: HashSet<&str> = words.into_iter().collect();
// Automatically deduplicates
Pattern 3: Fallible Collection
Rust
// Collect Result<T> into Result<Vec<T>>
let nums: Result<Vec<i32>, _> = ["1", "2", "3"]
.iter()
.map(|s| s.parse::<i32>())
.collect();
// Ok([1, 2, 3]) or Err on first failure
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Default | List is natural | Must specify type |
| To set | Manual fold_left | .collect::<HashSet<_>>() |
| To map | Manual fold_left | .collect::<HashMap<K,V>>() |
| Type inference | From context | Annotation or turbofish |
| Fallible | Manual error handling | .collect::<Result<Vec<_>,_>>() |
Exercises
Vec<(String, Vec<i32>)> into a HashMap<String, Vec<i32>> using collect().collect::<String>() to join a vector of characters with an uppercase transformation.Vec<Result<i32, String>> into Result<Vec<i32>, String>, then separately collect all errors using partition().