ExamplesBy LevelBy TopicLearning Paths
900 Intermediate

900-iterator-chain — Iterator Chain

Functional Programming

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

  • • Use .chain() to concatenate two iterators lazily without allocation
  • • Chain more than two iterators using consecutive .chain() calls
  • • Combine filtered sub-sequences with chain (evens-then-odds pattern)
  • • Use chain with sum and other consumers without collecting intermediate results
  • • Compare with OCaml's List.append which always allocates
  • Code 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

  • Allocation: Rust .chain() is zero-allocation until consumed; OCaml List.append allocates the first list's spine immediately.
  • Laziness: Rust chain is lazy; Seq.append is also lazy; List.append is eager.
  • Type requirements: Both ends of .chain() must yield the same Item type; OCaml List.append requires the same list element type.
  • Sum without collect: Rust 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]);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Eager concatval (@) : 'a list -> 'a list -> 'a list[a, b].concat() / Vec::extend
    Lazy chainval Seq.append : 'a Seq.t -> 'a Seq.t -> 'a Seq.tfn chain<U>(self, other: U) -> Chain<Self, U>
    Element copyimplicit (boxed OCaml values).copied() (explicit, T: Copy)

    Key Insights

  • Allocation model: OCaml's @ operator always allocates a new list immediately. Rust's .chain() defers everything — nothing is materialized until you call .collect() or iterate.
  • Lazy by default: Rust iterators are state machines describing what to do, not results. .chain() returns a Chain<A, B> struct that holds both iterators; it advances A until exhausted, then advances B.
  • OCaml Seq is the real parallel: 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.
  • Zero-cost abstraction: The 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.
  • Composability: .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

  • Write interleave<T: Copy>(a: &[T], b: &[T]) -> Vec<T> using zip and chain that produces [a0, b0, a1, b1, ...].
  • Implement unique_chain(a: &[i32], b: &[i32]) -> Vec<i32> using chain followed by a deduplication pass.
  • Use chain to implement a priority iterator that yields all high-priority items first, then normal-priority items.
  • Open Source Repos