900-iterator-chain — Iterator Chain
Tutorial
The Problem
Concatenating sequences without allocating a combined container is a common optimization. Iterating over "all items from set A, then all items from set B" should not require copying both into a new buffer. SQL's UNION ALL, Haskell's (++), and OCaml's List.append all solve this problem — with different allocation behaviors. Rust's .chain() adapter concatenates two iterators lazily: no allocation occurs until the combined sequence is consumed. This enables zero-cost concatenation of slices, ranges, filtered views, and any other iterator.
🎯 Learning Outcomes
.chain() to concatenate two iterators lazily without allocation.chain() callschain with sum and other consumers without collecting intermediate resultsList.append which always allocatesCode Example
// Rust: .chain() — lazy, zero extra allocation
let first = [1, 2, 3];
let second = [4, 5, 6];
let chained: Vec<i32> = first.iter().chain(second.iter()).copied().collect();Key Differences
.chain() is zero-allocation until consumed; OCaml List.append allocates the first list's spine immediately.Seq.append is also lazy; List.append is eager..chain() must yield the same Item type; OCaml List.append requires the same list element type.chain().sum() avoids materializing the combined list; OCaml requires List.append then List.fold_left (+) 0.OCaml Approach
List.append: 'a list -> 'a list -> 'a list creates a new list by copying the first list's spine. (@) is the infix alias. For lazy concatenation, Seq.append: 'a Seq.t -> 'a Seq.t -> 'a Seq.t works like Rust's .chain(). List.append is O(n) where n is the length of the first list; it allocates a new list spine. Chaining multiple lists: List.concat: 'a list list -> 'a list flattens a list of lists.
Full Source
#![allow(clippy::all)]
//! 256. Chaining Iterators with chain()
//!
//! `chain()` concatenates two iterators lazily — no allocation for the combination,
//! just composition. The two source iterators are traversed in sequence.
/// Chain two slices into a collected Vec without allocating a combined slice.
pub fn chain_slices<T: Copy>(first: &[T], second: &[T]) -> Vec<T> {
first.iter().chain(second.iter()).copied().collect()
}
/// Chain any two iterators that yield the same item type.
pub fn chain_iters<I, J, T>(a: I, b: J) -> Vec<T>
where
I: Iterator<Item = T>,
J: Iterator<Item = T>,
{
a.chain(b).collect()
}
/// Evens first, then odds — demonstrates chaining filtered iterators.
pub fn evens_then_odds(n: i32) -> Vec<i32> {
let evens = (0..n).filter(|x| x % 2 == 0);
let odds = (0..n).filter(|x| x % 2 != 0);
evens.chain(odds).collect()
}
/// Chain three slices in sequence.
pub fn chain_three<T: Copy>(a: &[T], b: &[T], c: &[T]) -> Vec<T> {
a.iter().chain(b.iter()).chain(c.iter()).copied().collect()
}
/// Sum a chain of two slices without collecting — fully lazy.
pub fn sum_chained(first: &[i32], second: &[i32]) -> i32 {
first.iter().chain(second.iter()).sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_slices_integers() {
let result = chain_slices(&[1, 2, 3], &[4, 5, 6]);
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_chain_slices_empty_first() {
let result = chain_slices::<i32>(&[], &[1, 2, 3]);
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_chain_slices_empty_second() {
let result = chain_slices(&[1, 2, 3], &[]);
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_chain_slices_both_empty() {
let result = chain_slices::<i32>(&[], &[]);
assert_eq!(result, vec![]);
}
#[test]
fn test_evens_then_odds() {
let result = evens_then_odds(6);
assert_eq!(result, vec![0, 2, 4, 1, 3, 5]);
}
#[test]
fn test_chain_three() {
let result = chain_three(&[1], &[2, 3], &[4, 5, 6]);
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_sum_chained() {
let total = sum_chained(&[1, 2, 3], &[4, 5, 6]);
assert_eq!(total, 21);
}
#[test]
fn test_chain_iters_ranges() {
let result = chain_iters(0..3_i32, 10..13_i32);
assert_eq!(result, vec![0, 1, 2, 10, 11, 12]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_slices_integers() {
let result = chain_slices(&[1, 2, 3], &[4, 5, 6]);
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_chain_slices_empty_first() {
let result = chain_slices::<i32>(&[], &[1, 2, 3]);
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_chain_slices_empty_second() {
let result = chain_slices(&[1, 2, 3], &[]);
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_chain_slices_both_empty() {
let result = chain_slices::<i32>(&[], &[]);
assert_eq!(result, vec![]);
}
#[test]
fn test_evens_then_odds() {
let result = evens_then_odds(6);
assert_eq!(result, vec![0, 2, 4, 1, 3, 5]);
}
#[test]
fn test_chain_three() {
let result = chain_three(&[1], &[2, 3], &[4, 5, 6]);
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_sum_chained() {
let total = sum_chained(&[1, 2, 3], &[4, 5, 6]);
assert_eq!(total, 21);
}
#[test]
fn test_chain_iters_ranges() {
let result = chain_iters(0..3_i32, 10..13_i32);
assert_eq!(result, vec![0, 1, 2, 10, 11, 12]);
}
}
Deep Comparison
OCaml vs Rust: Chaining Iterators with chain()
Side-by-Side Code
OCaml
(* OCaml: @ operator (List.append) — eager, allocates a new list *)
let first = [1; 2; 3]
let second = [4; 5; 6]
let chained = first @ second (* new list allocated here *)
(* Lazy alternative: Seq.append *)
let seq1 = List.to_seq [1; 2; 3]
let seq2 = List.to_seq [4; 5; 6]
let lazy_chain = Seq.append seq1 seq2 (* no allocation until iterated *)
Rust (idiomatic)
// Rust: .chain() — lazy, zero extra allocation
let first = [1, 2, 3];
let second = [4, 5, 6];
let chained: Vec<i32> = first.iter().chain(second.iter()).copied().collect();
Rust (functional/generic)
// Works over any two iterators of matching type
fn chain_iters<I, J, T>(a: I, b: J) -> Vec<T>
where
I: Iterator<Item = T>,
J: Iterator<Item = T>,
{
a.chain(b).collect()
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Eager concat | val (@) : 'a list -> 'a list -> 'a list | [a, b].concat() / Vec::extend |
| Lazy chain | val Seq.append : 'a Seq.t -> 'a Seq.t -> 'a Seq.t | fn chain<U>(self, other: U) -> Chain<Self, U> |
| Element copy | implicit (boxed OCaml values) | .copied() (explicit, T: Copy) |
Key Insights
@ operator always allocates a new list immediately. Rust's .chain() defers everything — nothing is materialized until you call .collect() or iterate..chain() returns a Chain<A, B> struct that holds both iterators; it advances A until exhausted, then advances B.Seq.append in OCaml is genuinely lazy, matching Rust's .chain() in spirit. The common @ operator is the eager path — the idiomatic OCaml shortcut that doesn't scale to large data.Chain iterator compiles to the same machine code as a hand-written loop that processes first then second. There is no virtual dispatch, no heap allocation for the combinator itself..chain() can be stacked: a.chain(b).chain(c) builds a Chain<Chain<A, B>, C> — all resolved at compile time, all zero cost. OCaml's Seq.append composes similarly but through closures rather than monomorphized types.When to Use Each Style
**Use .chain() on iterators when:** you want to process two sequences in order without allocating a combined collection — e.g., streaming log entries from multiple sources, combining filtered results, or building pipelines over large datasets.
**Use .collect() after .chain() when:** you need a concrete Vec for random access, repeated iteration, or passing to an API that requires owned data. The allocation happens exactly once, at the point you call .collect().
Exercises
interleave<T: Copy>(a: &[T], b: &[T]) -> Vec<T> using zip and chain that produces [a0, b0, a1, b1, ...].unique_chain(a: &[i32], b: &[i32]) -> Vec<i32> using chain followed by a deduplication pass.chain to implement a priority iterator that yields all high-priority items first, then normal-priority items.