ExamplesBy LevelBy TopicLearning Paths
901 Intermediate

901-iterator-zip — Iterator Zip

Functional Programming

Tutorial

The Problem

Pairing elements from two sequences is fundamental to many algorithms: matching names with scores, computing dot products, running pairwise comparisons, building dictionaries from key and value slices. OCaml's List.combine (zip) and List.split (unzip) are the eager equivalents. Rust's .zip() adapter is lazy and does not panic on length mismatch — it stops at the shorter iterator. This laziness makes zip safe for use with infinite iterators. The .enumerate() method is a special case of zip with an index sequence, and .unzip() is the inverse consumer.

🎯 Learning Outcomes

  • • Use .zip() to pair elements from two iterators lazily
  • • Build a HashMap by zipping keys and values
  • • Use .enumerate() as zip-with-index (equivalent to OCaml's List.mapi)
  • • Use .unzip() to split an iterator of pairs into two collections
  • • Understand that .zip() stops at the shorter iterator — no panic on length mismatch
  • Code Example

    let names = ["Alice", "Bob", "Carol"];
    let scores = [95u32, 87, 92];
    let paired: Vec<_> = names.iter().zip(scores.iter()).collect();
    // stops silently at shorter — no panic
    
    let indexed: Vec<_> = ["a", "b", "c"].iter().enumerate().collect();

    Key Differences

  • Length mismatch: Rust .zip() silently truncates; OCaml List.combine raises Invalid_argumentSeq.zip truncates like Rust.
  • Laziness: Rust .zip() is lazy; OCaml List.combine is eager.
  • unzip: Rust .unzip() works on any Iterator<Item=(A,B)>; OCaml List.split only works on lists.
  • enumerate: Both provide enumerate / mapi; OCaml's mapi maps immediately, Rust's enumerate() is lazy.
  • OCaml Approach

    List.combine: 'a list -> 'b list -> ('a * 'b) list panics (Invalid_argument) on unequal lengths — it expects exact match. List.split: ('a * 'b) list -> 'a list * 'b list is the inverse. List.mapi: (int -> 'a -> 'b) -> 'a list -> 'b list provides index + element. For lazy zip: Seq.zip: 'a Seq.t -> 'b Seq.t -> ('a * 'b) Seq.t (since OCaml 4.14), which truncates like Rust's .zip(). Building a Hashtbl from two lists: List.combine keys values |> List.iter (fun (k, v) -> Hashtbl.add tbl k v).

    Full Source

    #![allow(clippy::all)]
    //! 257. Pairing elements with zip()
    //!
    //! `zip()` pairs elements from two iterators, stopping at the shorter one.
    //! Like OCaml's `List.combine`, but lazy and infallible — no panic on length mismatch.
    
    use std::collections::HashMap;
    
    /// Pair two slices element-by-element, returning a Vec of tuples.
    /// Stops at the shorter slice — never panics on length mismatch.
    pub fn zip_slices<A: Copy, B: Copy>(a: &[A], b: &[B]) -> Vec<(A, B)> {
        a.iter().zip(b.iter()).map(|(&x, &y)| (x, y)).collect()
    }
    
    /// Pair names and scores into a HashMap.
    pub fn names_to_scores<'a>(names: &[&'a str], scores: &[u32]) -> HashMap<&'a str, u32> {
        names
            .iter()
            .zip(scores.iter())
            .map(|(&name, &score)| (name, score))
            .collect()
    }
    
    /// Enumerate items: pair each element with its index (like `List.mapi` in OCaml).
    pub fn indexed<T: Copy>(items: &[T]) -> Vec<(usize, T)> {
        items.iter().copied().enumerate().collect()
    }
    
    /// Unzip a Vec of pairs back into two Vecs (inverse of zip).
    pub fn unzip_pairs<A, B>(pairs: Vec<(A, B)>) -> (Vec<A>, Vec<B>) {
        pairs.into_iter().unzip()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_zip_equal_length() {
            let a = [1i32, 2, 3];
            let b = [10i32, 20, 30];
            assert_eq!(zip_slices(&a, &b), vec![(1, 10), (2, 20), (3, 30)]);
        }
    
        #[test]
        fn test_zip_truncates_at_shorter() {
            let long = [1i32, 2, 3, 4, 5];
            let short = [10i32, 20];
            let result = zip_slices(&long, &short);
            assert_eq!(result.len(), 2);
            assert_eq!(result, vec![(1, 10), (2, 20)]);
        }
    
        #[test]
        fn test_zip_empty() {
            let a: [i32; 0] = [];
            let b = [1i32, 2, 3];
            assert_eq!(zip_slices(&a, &b), vec![]);
        }
    
        #[test]
        fn test_names_to_scores() {
            let names = ["Alice", "Bob", "Carol"];
            let scores = [95u32, 87, 92];
            let map = names_to_scores(&names, &scores);
            assert_eq!(map["Alice"], 95);
            assert_eq!(map["Bob"], 87);
            assert_eq!(map["Carol"], 92);
        }
    
        #[test]
        fn test_indexed() {
            let items = ['a', 'b', 'c'];
            assert_eq!(indexed(&items), vec![(0, 'a'), (1, 'b'), (2, 'c')]);
        }
    
        #[test]
        fn test_unzip_roundtrip() {
            let pairs = vec![(1i32, 'a'), (2, 'b'), (3, 'c')];
            let (nums, chars) = unzip_pairs(pairs);
            assert_eq!(nums, vec![1, 2, 3]);
            assert_eq!(chars, vec!['a', 'b', 'c']);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_zip_equal_length() {
            let a = [1i32, 2, 3];
            let b = [10i32, 20, 30];
            assert_eq!(zip_slices(&a, &b), vec![(1, 10), (2, 20), (3, 30)]);
        }
    
        #[test]
        fn test_zip_truncates_at_shorter() {
            let long = [1i32, 2, 3, 4, 5];
            let short = [10i32, 20];
            let result = zip_slices(&long, &short);
            assert_eq!(result.len(), 2);
            assert_eq!(result, vec![(1, 10), (2, 20)]);
        }
    
        #[test]
        fn test_zip_empty() {
            let a: [i32; 0] = [];
            let b = [1i32, 2, 3];
            assert_eq!(zip_slices(&a, &b), vec![]);
        }
    
        #[test]
        fn test_names_to_scores() {
            let names = ["Alice", "Bob", "Carol"];
            let scores = [95u32, 87, 92];
            let map = names_to_scores(&names, &scores);
            assert_eq!(map["Alice"], 95);
            assert_eq!(map["Bob"], 87);
            assert_eq!(map["Carol"], 92);
        }
    
        #[test]
        fn test_indexed() {
            let items = ['a', 'b', 'c'];
            assert_eq!(indexed(&items), vec![(0, 'a'), (1, 'b'), (2, 'c')]);
        }
    
        #[test]
        fn test_unzip_roundtrip() {
            let pairs = vec![(1i32, 'a'), (2, 'b'), (3, 'c')];
            let (nums, chars) = unzip_pairs(pairs);
            assert_eq!(nums, vec![1, 2, 3]);
            assert_eq!(chars, vec!['a', 'b', 'c']);
        }
    }

    Deep Comparison

    OCaml vs Rust: Pairing Elements with zip()

    Side-by-Side Code

    OCaml

    let names = ["Alice"; "Bob"; "Carol"] in
    let scores = [95; 87; 92] in
    let paired = List.combine names scores in
    (* paired: [("Alice", 95); ("Bob", 87); ("Carol", 92)] *)
    
    (* List.combine raises Invalid_argument on length mismatch *)
    let indexed = List.mapi (fun i x -> (i, x)) ["a"; "b"; "c"]
    

    Rust (idiomatic)

    let names = ["Alice", "Bob", "Carol"];
    let scores = [95u32, 87, 92];
    let paired: Vec<_> = names.iter().zip(scores.iter()).collect();
    // stops silently at shorter — no panic
    
    let indexed: Vec<_> = ["a", "b", "c"].iter().enumerate().collect();
    

    Rust (functional — build HashMap via zip)

    let map: HashMap<&str, u32> = names.iter()
        .zip(scores.iter())
        .map(|(&name, &score)| (name, score))
        .collect();
    

    Type Signatures

    ConceptOCamlRust
    Zip two listsList.combine : 'a list -> 'b list -> ('a * 'b) list.zip() -> impl Iterator<Item=(A,B)>
    EnumerateList.mapi : (int -> 'a -> 'b) -> 'a list -> 'b list.enumerate() -> impl Iterator<Item=(usize,T)>
    UnzipList.split : ('a * 'b) list -> 'a list * 'b list.unzip() -> (Vec<A>, Vec<B>)
    Pair type'a * 'b(A, B)

    Key Insights

  • Error handling: OCaml's List.combine raises Invalid_argument on length mismatch; Rust's zip() silently truncates at the shorter iterator — the safe, panic-free choice.
  • Laziness: Rust's zip() is lazy — it produces pairs on demand without allocating. OCaml's List.combine eagerly builds a new list. Add .collect() in Rust when you need a concrete Vec.
  • Enumerate as zip: OCaml uses List.mapi to pair indices with elements; Rust uses .enumerate(), which is zip(0..) in disguise — both express the same intent, but Rust's name is more discoverable.
  • Unzip symmetry: Both languages provide the inverse operation (List.split / .unzip()), making round-trips straightforward. Rust's .unzip() is a collector, so the types are inferred from context.
  • Composability: Because zip() returns an iterator, you can chain further adapters (.map(), .filter(), .flat_map()) before collecting — OCaml achieves the same with List.map applied to the combined list, but Rust avoids the intermediate allocation.
  • When to Use Each Style

    **Use idiomatic Rust (.zip().collect())** when you need a Vec of pairs for later use or when feeding into .collect::<HashMap<_,_>>(). **Use .zip() inline in a for loop or .for_each()** when you only need to process pairs once and don't want to allocate — the most common pattern and zero overhead.

    Exercises

  • Write dot_product(a: &[f64], b: &[f64]) -> f64 using .zip().map(|(x,y)| x*y).sum().
  • Implement zip_with_default<T: Clone>(a: &[T], b: &[T], default: T) -> Vec<(T, T)> that pads the shorter slice.
  • Use zip and scan together to compute the running difference between two parallel time series.
  • Open Source Repos