901-iterator-zip — Iterator Zip
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
.zip() to pair elements from two iterators lazilyHashMap by zipping keys and values.enumerate() as zip-with-index (equivalent to OCaml's List.mapi).unzip() to split an iterator of pairs into two collections.zip() stops at the shorter iterator — no panic on length mismatchCode 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
.zip() silently truncates; OCaml List.combine raises Invalid_argument — Seq.zip truncates like Rust..zip() is lazy; OCaml List.combine is eager..unzip() works on any Iterator<Item=(A,B)>; OCaml List.split only works on lists.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']);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Zip two lists | List.combine : 'a list -> 'b list -> ('a * 'b) list | .zip() -> impl Iterator<Item=(A,B)> |
| Enumerate | List.mapi : (int -> 'a -> 'b) -> 'a list -> 'b list | .enumerate() -> impl Iterator<Item=(usize,T)> |
| Unzip | List.split : ('a * 'b) list -> 'a list * 'b list | .unzip() -> (Vec<A>, Vec<B>) |
| Pair type | 'a * 'b | (A, B) |
Key Insights
List.combine raises Invalid_argument on length mismatch; Rust's zip() silently truncates at the shorter iterator — the safe, panic-free choice.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.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.List.split / .unzip()), making round-trips straightforward. Rust's .unzip() is a collector, so the types are inferred from context.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
dot_product(a: &[f64], b: &[f64]) -> f64 using .zip().map(|(x,y)| x*y).sum().zip_with_default<T: Clone>(a: &[T], b: &[T], default: T) -> Vec<(T, T)> that pads the shorter slice.zip and scan together to compute the running difference between two parallel time series.