885-zip-unzip — Zip and Unzip
Tutorial
The Problem
Pairing two sequences element-by-element and splitting a sequence of pairs back into two separate sequences are fundamental data transformations. Mathematically, zip and unzip are inverses. Practically, zip is used for: combining coordinates with labels, computing dot products, running element-wise comparisons, and building key-value pairs. OCaml provides List.combine (zip) and List.split (unzip). Python has built-in zip(). Rust's .zip() adapter on iterators handles the pairing lazily, and .unzip() consumer splits pairs back. This example covers zip, zip_with (pairwise operations), zip_with_index, and zip_longest.
🎯 Learning Outcomes
.zip() to pair elements from two iterators, stopping at the shorter one.unzip() to split an iterator of pairs into two collectionsdot_product and pairwise_max as zip-based operations.enumerate() as zip-with-indexzip_longest using explicit length comparison for paddingCode Example
pub fn zip_vecs(a: &[i32], b: &[&str]) -> Vec<(i32, String)> {
a.iter()
.zip(b.iter())
.map(|(&n, &s)| (n, s.to_string()))
.collect()
}
pub fn unzip_vecs(pairs: &[(i32, &str)]) -> (Vec<i32>, Vec<String>) {
pairs.iter().map(|&(n, s)| (n, s.to_string())).unzip()
}
pub fn dot_product(a: &[i32], b: &[i32]) -> i32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}Key Differences
.zip() silently stops at the shorter iterator; OCaml List.combine raises an exception on unequal lengths..zip() is lazy; OCaml List.combine is eager and allocates immediately..unzip() works on any Iterator<Item = (A, B)>; OCaml's List.split only works on lists..zip().map(|(a, b)| f(a, b)); OCaml has dedicated List.map2 f xs ys.OCaml Approach
OCaml's List.combine: 'a list -> 'b list -> ('a * 'b) list is the eager zip. It raises Invalid_argument on unequal lengths — unlike Rust's silent truncation. List.split: ('a * 'b) list -> 'a list * 'b list is the unzip. List.map2 f xs ys is zip-with for same-length lists. List.mapi (fun i x -> (i, x)) xs is zip-with-index. OCaml lacks a standard zip_longest — it must be implemented with explicit recursion.
Full Source
#![allow(clippy::all)]
// Example 091: Zip and Unzip
// OCaml List.combine/split → Rust zip/unzip
// === Approach 1: Basic zip/unzip ===
/// Pair two slices element-by-element (stops at shorter).
/// Mirrors OCaml's `List.combine`.
pub fn zip_vecs(a: &[i32], b: &[&str]) -> Vec<(i32, String)> {
a.iter()
.zip(b.iter())
.map(|(&n, &s)| (n, s.to_string()))
.collect()
}
/// Split a slice of pairs back into two Vecs.
/// Mirrors OCaml's `List.split`.
pub fn unzip_vecs(pairs: &[(i32, &str)]) -> (Vec<i32>, Vec<String>) {
pairs.iter().map(|&(n, s)| (n, s.to_string())).unzip()
}
// === Approach 2: zip_with / map2 equivalent ===
/// Element-wise dot product — zip then fold.
pub fn dot_product(a: &[i32], b: &[i32]) -> i32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
/// Element-wise maximum of two slices.
pub fn pairwise_max(a: &[i32], b: &[i32]) -> Vec<i32> {
a.iter().zip(b.iter()).map(|(&x, &y)| x.max(y)).collect()
}
/// General element-wise operation (`zip_with` / `List.map2`).
pub fn pairwise_op<T, U>(a: &[T], b: &[T], f: impl Fn(&T, &T) -> U) -> Vec<U> {
a.iter().zip(b.iter()).map(|(x, y)| f(x, y)).collect()
}
// === Approach 3: zip with index (enumerate) ===
/// Attach 0-based indices to each element.
/// Mirrors OCaml's `List.mapi (fun i x -> (i, x))`.
pub fn zip_with_index<T: Clone>(lst: &[T]) -> Vec<(usize, T)> {
lst.iter()
.enumerate()
.map(|(i, x)| (i, x.clone()))
.collect()
}
// === Approach 4: zip_longest — pad shorter sequence ===
/// Zip two slices, padding the shorter one with supplied defaults.
/// OCaml's `zip_longest` with `~default_a` / `~default_b`.
pub fn zip_longest<T: Clone>(a: &[T], b: &[T], default_a: T, default_b: T) -> Vec<(T, T)> {
let len = a.len().max(b.len());
(0..len)
.map(|i| {
let x = a.get(i).cloned().unwrap_or_else(|| default_a.clone());
let y = b.get(i).cloned().unwrap_or_else(|| default_b.clone());
(x, y)
})
.collect()
}
// === Approach 5: Unzip owned pairs (generic) ===
/// Unzip a Vec of owned pairs into two Vecs.
pub fn unzip_owned<A, B>(pairs: Vec<(A, B)>) -> (Vec<A>, Vec<B>) {
pairs.into_iter().unzip()
}
#[cfg(test)]
mod tests {
use super::*;
// --- zip_vecs / unzip_vecs ---
#[test]
fn test_zip_empty() {
let result: Vec<(i32, String)> = zip_vecs(&[], &[]);
assert_eq!(result, vec![]);
}
#[test]
fn test_zip_equal_length() {
let a = [1, 2, 3];
let b = ["one", "two", "three"];
let result = zip_vecs(&a, &b);
assert_eq!(
result,
vec![
(1, "one".to_string()),
(2, "two".to_string()),
(3, "three".to_string()),
]
);
}
#[test]
fn test_zip_truncates_at_shorter() {
// b is shorter — Rust's zip stops at the shorter iterator
let a = [1, 2, 3, 4];
let b = ["a", "b"];
let result = zip_vecs(&a, &b);
assert_eq!(result, vec![(1, "a".to_string()), (2, "b".to_string())]);
}
#[test]
fn test_unzip_roundtrip() {
let pairs = [(10, "x"), (20, "y"), (30, "z")];
let (nums, strs) = unzip_vecs(&pairs);
assert_eq!(nums, vec![10, 20, 30]);
assert_eq!(strs, vec!["x", "y", "z"]);
}
// --- dot_product ---
#[test]
fn test_dot_product_basic() {
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32); // 4+10+18
}
#[test]
fn test_dot_product_zeros() {
assert_eq!(dot_product(&[0, 0, 0], &[1, 2, 3]), 0);
}
// --- pairwise_max ---
#[test]
fn test_pairwise_max() {
assert_eq!(pairwise_max(&[1, 5, 3], &[4, 2, 6]), vec![4, 5, 6]);
}
// --- pairwise_op ---
#[test]
fn test_pairwise_op_add() {
let result = pairwise_op(&[1, 2, 3], &[10, 20, 30], |a, b| a + b);
assert_eq!(result, vec![11, 22, 33]);
}
// --- zip_with_index ---
#[test]
fn test_zip_with_index_empty() {
let result: Vec<(usize, i32)> = zip_with_index(&[]);
assert_eq!(result, vec![]);
}
#[test]
fn test_zip_with_index_basic() {
let result = zip_with_index(&["a", "b", "c"]);
assert_eq!(result, vec![(0, "a"), (1, "b"), (2, "c")]);
}
// --- zip_longest ---
#[test]
fn test_zip_longest_equal() {
let result = zip_longest(&[1, 2], &[3, 4], 0, 0);
assert_eq!(result, vec![(1, 3), (2, 4)]);
}
#[test]
fn test_zip_longest_a_shorter() {
let result = zip_longest(&[1], &[10, 20, 30], 0, 0);
assert_eq!(result, vec![(1, 10), (0, 20), (0, 30)]);
}
#[test]
fn test_zip_longest_b_shorter() {
let result = zip_longest(&[1, 2, 3], &[9], 0, 0);
assert_eq!(result, vec![(1, 9), (2, 0), (3, 0)]);
}
// --- unzip_owned ---
#[test]
fn test_unzip_owned() {
let pairs = vec![(1, "a"), (2, "b"), (3, "c")];
let (nums, letters): (Vec<i32>, Vec<&str>) = unzip_owned(pairs);
assert_eq!(nums, vec![1, 2, 3]);
assert_eq!(letters, vec!["a", "b", "c"]);
}
}#[cfg(test)]
mod tests {
use super::*;
// --- zip_vecs / unzip_vecs ---
#[test]
fn test_zip_empty() {
let result: Vec<(i32, String)> = zip_vecs(&[], &[]);
assert_eq!(result, vec![]);
}
#[test]
fn test_zip_equal_length() {
let a = [1, 2, 3];
let b = ["one", "two", "three"];
let result = zip_vecs(&a, &b);
assert_eq!(
result,
vec![
(1, "one".to_string()),
(2, "two".to_string()),
(3, "three".to_string()),
]
);
}
#[test]
fn test_zip_truncates_at_shorter() {
// b is shorter — Rust's zip stops at the shorter iterator
let a = [1, 2, 3, 4];
let b = ["a", "b"];
let result = zip_vecs(&a, &b);
assert_eq!(result, vec![(1, "a".to_string()), (2, "b".to_string())]);
}
#[test]
fn test_unzip_roundtrip() {
let pairs = [(10, "x"), (20, "y"), (30, "z")];
let (nums, strs) = unzip_vecs(&pairs);
assert_eq!(nums, vec![10, 20, 30]);
assert_eq!(strs, vec!["x", "y", "z"]);
}
// --- dot_product ---
#[test]
fn test_dot_product_basic() {
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32); // 4+10+18
}
#[test]
fn test_dot_product_zeros() {
assert_eq!(dot_product(&[0, 0, 0], &[1, 2, 3]), 0);
}
// --- pairwise_max ---
#[test]
fn test_pairwise_max() {
assert_eq!(pairwise_max(&[1, 5, 3], &[4, 2, 6]), vec![4, 5, 6]);
}
// --- pairwise_op ---
#[test]
fn test_pairwise_op_add() {
let result = pairwise_op(&[1, 2, 3], &[10, 20, 30], |a, b| a + b);
assert_eq!(result, vec![11, 22, 33]);
}
// --- zip_with_index ---
#[test]
fn test_zip_with_index_empty() {
let result: Vec<(usize, i32)> = zip_with_index(&[]);
assert_eq!(result, vec![]);
}
#[test]
fn test_zip_with_index_basic() {
let result = zip_with_index(&["a", "b", "c"]);
assert_eq!(result, vec![(0, "a"), (1, "b"), (2, "c")]);
}
// --- zip_longest ---
#[test]
fn test_zip_longest_equal() {
let result = zip_longest(&[1, 2], &[3, 4], 0, 0);
assert_eq!(result, vec![(1, 3), (2, 4)]);
}
#[test]
fn test_zip_longest_a_shorter() {
let result = zip_longest(&[1], &[10, 20, 30], 0, 0);
assert_eq!(result, vec![(1, 10), (0, 20), (0, 30)]);
}
#[test]
fn test_zip_longest_b_shorter() {
let result = zip_longest(&[1, 2, 3], &[9], 0, 0);
assert_eq!(result, vec![(1, 9), (2, 0), (3, 0)]);
}
// --- unzip_owned ---
#[test]
fn test_unzip_owned() {
let pairs = vec![(1, "a"), (2, "b"), (3, "c")];
let (nums, letters): (Vec<i32>, Vec<&str>) = unzip_owned(pairs);
assert_eq!(nums, vec![1, 2, 3]);
assert_eq!(letters, vec!["a", "b", "c"]);
}
}
Deep Comparison
OCaml vs Rust: Zip and Unzip
Side-by-Side Code
OCaml
(* Basic zip/unzip — built-in *)
let zip = List.combine (* ('a list * 'b list) -> ('a * 'b) list *)
let unzip = List.split (* ('a * 'b) list -> ('a list * 'b list) *)
(* zip_with / map2 *)
let dot_product xs ys =
List.map2 ( * ) xs ys |> List.fold_left ( + ) 0
(* zip_with_index *)
let zip_with_index lst = List.mapi (fun i x -> (i, x)) lst
(* zip_longest — manual recursion *)
let rec zip_longest ~default_a ~default_b xs ys =
match xs, ys with
| [], [] -> []
| x :: xs', y :: ys' -> (x, y) :: zip_longest ~default_a ~default_b xs' ys'
| x :: xs', [] -> (x, default_b) :: zip_longest ~default_a ~default_b xs' []
| [], y :: ys' -> (default_a, y) :: zip_longest ~default_a ~default_b [] ys'
Rust (idiomatic — iterator adapters)
pub fn zip_vecs(a: &[i32], b: &[&str]) -> Vec<(i32, String)> {
a.iter()
.zip(b.iter())
.map(|(&n, &s)| (n, s.to_string()))
.collect()
}
pub fn unzip_vecs(pairs: &[(i32, &str)]) -> (Vec<i32>, Vec<String>) {
pairs.iter().map(|&(n, s)| (n, s.to_string())).unzip()
}
pub fn dot_product(a: &[i32], b: &[i32]) -> i32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
Rust (functional/recursive — zip_longest)
pub fn zip_longest<T: Clone>(a: &[T], b: &[T], default_a: T, default_b: T) -> Vec<(T, T)> {
let len = a.len().max(b.len());
(0..len)
.map(|i| {
let x = a.get(i).cloned().unwrap_or_else(|| default_a.clone());
let y = b.get(i).cloned().unwrap_or_else(|| default_b.clone());
(x, y)
})
.collect()
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| zip | val combine : 'a list -> 'b list -> ('a * 'b) list | .zip() iterator adapter → collect::<Vec<_>>() |
| unzip | val split : ('a * 'b) list -> 'a list * 'b list | .unzip() on iterator of pairs |
| zip_with | val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list | a.iter().zip(b).map(\|(x,y)| f(x,y)).collect() |
| enumerate | List.mapi (fun i x -> (i, x)) | .iter().enumerate() |
| zip_longest | custom recursion | index-based range loop + .get().unwrap_or_else() |
Key Insights
List.combine raises Invalid_argument if lists differ in length; Rust's .zip() silently stops at the shorter iterator — a quieter contract that may hide bugs..unzip() is a collector:** Rust expresses unzip as a special collect() destination rather than a standalone function, making it composable with arbitrary iterator chains.zip_with → iterator chain:** OCaml's List.map2 f xs ys becomes xs.iter().zip(ys).map(|(x,y)| f(x,y)) in Rust — more explicit but equally expressive.zip_longest needs manual work in both languages:** Neither OCaml stdlib nor Rust std provides a zip-longest; both require a custom implementation. Rust's index-based approach avoids explicit recursion..zip() in Rust works over references (&T), making it clear that neither input is consumed; .unzip() on an owned iterator transfers ownership into two output Vecs.When to Use Each Style
**Use idiomatic Rust (.zip() / .unzip())** when combining or splitting parallel slices/iterators in a pipeline — it chains naturally with .map(), .filter(), and .collect().
**Use zip_longest** when you cannot guarantee equal-length inputs and need to preserve all data rather than silently discard tails.
Exercises
weighted_sum(weights: &[f64], values: &[f64]) -> f64 using zip and fold.transpose_matrix<T: Clone>(matrix: &[Vec<T>]) -> Vec<Vec<T>> using zip over rows.merge_sorted using a peekable zip that merges two sorted slices maintaining sort order.