Nugget 10

Home

The Lazy Pipeline

You call .map() and nothing happens. Then .filter() and still nothing. Finally .collect() and it all runs. This is the single most important thing to understand about Rust iterators.

The Surprise

let v = vec![1, 2, 3, 4, 5];
v.iter().map(|x| {
    println!("mapping {x}");
    x * 2
});
// Nothing prints! No error, no output.

In JavaScript, arr.map(fn) runs immediately and returns a new array. In Rust, map() just describes what should happen — it doesn't actually do the work. The iterator is lazy.

Lazy Means "Not Until You Ask"

map() returns a new iterator that remembers the transformation. The actual work only happens when something calls next():

let v = vec![1, 2, 3];
let mapped = v.iter().map(|x| x * 10);  // Nothing computed yet

let mut iter = mapped;
assert_eq!(iter.next(), Some(10));  // Now 1*10 happens
assert_eq!(iter.next(), Some(20));  // Now 2*10 happens
assert_eq!(iter.next(), Some(30));  // Now 3*10 happens
assert_eq!(iter.next(), None);      // Done

Each next() pulls one item through the entire pipeline. One item at a time, no intermediate storage.

Chaining Adapters

You can chain multiple transformations — they compose into a single lazy iterator that only runs when consumed:

let result: Vec<i32> = (1..=10)
    .map(|x| x * 2)          // Lazy: "remember to double each item"
    .filter(|x| x > 10)      // Lazy: "remember to keep only big ones"
    .collect();               // Eager: NOW do all the work

println!("{:?}", result);     // [12, 14, 16, 18, 20]

// collect() pulls items through: 
// 1→2→no, 2→4→no, 3→6→no, 4→8→no, 5→10→no,
// 6→12→yes→store, 7→14→yes→store, ...

Each item flows through the entire pipeline before the next one starts. No intermediate Vec is ever created between map and filter.

JavaScript Comparison

// JavaScript — eager, creates intermediate arrays
[1, 2, 3, 4, 5]
    .map(x => x * 2)       // Creates [2,4,6,8,10] — full allocation
    .filter(x => x > 5);   // Creates [6,8,10] — second allocation

// Two arrays allocated, one thrown away.
// Rust — lazy, no intermediate allocation
(1..=5)
    .map(|x| x * 2)        // Just remembers the function
    .filter(|x| x > 5)     // Just remembers another function
    .collect();             // One pass, one allocation

This is a fundamental design difference. Rust's approach means you can chain freely without worrying about extra allocations. It also means you can work with infinite sequences — more on that next.

Bonus: Infinite Iterators

Because iterators are lazy, you can create infinite sequences and only take what you need:

// Infinite range, but we only take 5
let first_five: Vec<i32> = (0..)
    .skip(10)
    .take(5)
    .collect();

println!("{:?}", first_five); // [10, 11, 12, 13, 14]

// This would OOM in JS (or hang):
// [...Array(Infinity).keys()].map(x => x).filter(...).

// Rust is fine — nothing runs until collect() with .take() limiting it.

💡 0.. is an infinite range

(0..) starts at 0 and goes forever. It's only useful when paired with take(), skip(), or another adapter that limits the output.

Key Takeaways