ExamplesBy LevelBy TopicLearning Paths
288 Intermediate

288: Materializing Iterators with collect()

Functional Programming

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

  • • Understand collect() as materializing a lazy iterator into any FromIterator<T> type
  • • Use type annotations (or turbofish ::<Vec<_>>()) to specify the output collection type
  • • Collect into HashSet for deduplication, HashMap from pairs, String from chars
  • • Recognize collect::<Result<Vec<T>, E>>() as the short-circuit pattern for fallible collection
  • Code Example

    let squares: Vec<u32> = (0..5).map(|x| x * x).collect();
    // Type annotation tells collect() what to produce

    Key Differences

  • Unified API: Rust's collect() works for all FromIterator types via one method; OCaml requires type-specific conversion functions.
  • Type-driven dispatch: The output type of collect() is selected by the compiler from type annotations alone — no conditional branching.
  • Fallible collection: collect::<Result<Vec<T>, E>>() aggregates results, short-circuiting on first error — OCaml requires explicit fold logic.
  • Custom types: Implementing 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]);
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    DefaultList is naturalMust specify type
    To setManual fold_left.collect::<HashSet<_>>()
    To mapManual fold_left.collect::<HashMap<K,V>>()
    Type inferenceFrom contextAnnotation or turbofish
    FallibleManual error handling.collect::<Result<Vec<_>,_>>()

    Exercises

  • Collect a Vec<(String, Vec<i32>)> into a HashMap<String, Vec<i32>> using collect().
  • Use collect::<String>() to join a vector of characters with an uppercase transformation.
  • Collect a Vec<Result<i32, String>> into Result<Vec<i32>, String>, then separately collect all errors using partition().
  • Open Source Repos