Skip to content
C Codeloom
Rust

Rust Iterators and Adapters

Master Rust's iterator trait, lazy adapters like map and filter, and consumers like collect and fold for fast, expressive data processing.

·3 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • The Iterator trait
  • Lazy adapters
  • Consumers
  • Custom iterators
  • Performance characteristics

Prerequisites

  • Basic familiarity with Rust

What and Why

Iterators are how Rust handles sequences. Instead of indexed for-loops, you build pipelines of small operations. The compiler then fuses them into tight loops via monomorphization, so high-level code generates the same assembly you’d write by hand.

The whole system is built on one trait, Iterator, with one required method, next. Everything else is default methods or adapter types built around that.

Mental Model

An iterator is a lazy stream. Adapters like map, filter, and take return new iterators that wrap the original. Nothing runs until a consumer like collect, sum, for_each, or a for loop pulls values through. This laziness lets the compiler optimize away the intermediate types.

vec.iter()      --> Iter<i32>
 .filter(...)  --> Filter<Iter<i32>, F>
 .map(...)     --> Map<Filter<...>, G>
 .sum::<i32>() --> i32  (runs the pipeline)
Iterator pipeline

Hands-on Example

fn main() {
    let nums = vec![1, 2, 3, 4, 5, 6];

    let sum_of_squares: i32 = nums.iter()
        .filter(|&&n| n % 2 == 0)
        .map(|&n| n * n)
        .sum();

    println!("{}", sum_of_squares); // 56

    // Build a HashMap
    use std::collections::HashMap;
    let lengths: HashMap<&str, usize> = ["apple", "fig", "banana"]
        .iter()
        .map(|s| (*s, s.len()))
        .collect();

    // Chain and enumerate
    for (i, x) in nums.iter().chain(&[100, 200]).enumerate() {
        println!("{i}: {x}");
    }
}

Three flavors of iterator from a collection: iter() (by reference), iter_mut() (mutable reference), and into_iter() (by value, consuming).

Custom Iterator

struct Fib { a: u64, b: u64 }

impl Iterator for Fib {
    type Item = u64;
    fn next(&mut self) -> Option<u64> {
        let next = self.a;
        self.a = self.b;
        self.b = next + self.b;
        Some(next)
    }
}

fn main() {
    let fibs: Vec<u64> = Fib { a: 0, b: 1 }.take(10).collect();
    println!("{:?}", fibs);
}

Implementing Iterator once unlocks every adapter automatically.

Common Pitfalls

Forgetting iterators are lazy. nums.iter().map(|x| println!("{x}")); prints nothing because no consumer ever pulls. Add .for_each(|_| {}) or just use a for loop.

Borrow vs ownership confusion. for x in vec consumes the vector; for x in &vec borrows. Adapters compose from whichever you start with.

collect type annotations. let v = ... .collect(); doesn’t compile without telling the compiler what to collect into. Use .collect::<Vec<_>>() or annotate the binding type.

Overusing clone. A common reflex is sprinkling .cloned() in pipelines. Often you can pass references through and avoid the copy.

Allocation in hot loops. .collect::<Vec<_>>() between two steps allocates. Keep the pipeline going if you can; only collect at the boundaries.

Practical Tips

  • Use iter() for read-only, iter_mut() to modify in place, into_iter() when you no longer need the collection.
  • Prefer find, any, all over manual loops for early exit.
  • flat_map is your friend for unflattening one-to-many transformations.
  • fold and try_fold express many algorithms cleanly; try_fold short-circuits on Err or None.
  • For numeric work, sum, product, min, max are zero-cost over a typed iterator.
  • Reach for the itertools crate when you need windows, group-by, or unzipping multiple ways.

Wrap-up

Iterators are Rust’s superpower for data processing: expressive, safe, and zero-cost. Start by replacing loops with map/filter/collect, then learn the consumers, then implement your own when needed. The same pipeline reads like prose and compiles to a single tight loop, which is the kind of trade-off Rust was designed to make easy.