0x55aa
← Back to Blog

Rust Iterators: Lazy, Fast, and Making PHP Developers Cry 🦀🔄

9 min read

Rust Iterators: Lazy, Fast, and Making PHP Developers Cry 🦀🔄

Confession: When I first saw Rust iterators, I thought "cool, it's just like Laravel Collections." I was wrong. So, so wrong. It's Laravel Collections if Laravel Collections were on a strict diet, had a PhD in performance optimization, and could bench-press your entire server rack. 💪

Coming from 7 years of Laravel and Node.js, I was used to fluent, chainable collection methods. Loved them! But they all had a dirty secret: they compute eagerly. Every .map(), every .filter() — executed immediately, memory allocated, results stored. With a million records, you feel it.

Then I started writing Rust for my RF/SDR hobby projects where I'm processing millions of signal samples per second and... iterators changed everything.

What Even Is a Lazy Iterator? 🦥

The eager (PHP/JS) way:

// Laravel: Every step allocates a new collection!
$result = collect($millionItems)
    ->filter(fn($x) => $x > 0)      // Allocates 500k items
    ->map(fn($x) => $x * 2)         // Allocates 500k items
    ->take(10)                       // THEN discards 499,990!
    ->toArray();

You allocated memory for a million items, filtered to 500k, mapped all 500k, then threw away 499,990 of them. Your RAM is weeping. 😭

The lazy (Rust) way:

// Rust: Nothing runs until .collect()!
let result: Vec<i32> = big_vec.iter()
    .filter(|&&x| x > 0)   // Just a description, no work yet
    .map(|&x| x * 2)        // Still just a description
    .take(10)               // "I only need 10"
    .collect();             // NOW it runs - processing stops at 10!

Rust processes ONE element at a time, passes it through the whole chain, and stops the moment it has 10 results. Zero wasted allocations. 🤯

What excited me about this: For my RF projects processing 2.4 million samples per second, the difference between eager and lazy evaluation is the difference between "works great" and "your laptop catches fire." 📡🔥

The Iterator Trait: Elegantly Simple ✨

The whole magic comes from one stupidly simple interface:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

That's literally it. One method. If you can return Some(value) or None, you're an iterator. Everything else — map, filter, take, zip, chain, flat_map — is built ON TOP of this one method.

Coming from PHP: This is like every iterable in PHP implementing one interface, and then Laravel Collection being just a WRAPPER that adds 80 methods. Except in Rust, those 80 methods are ZERO-COST because they compose without allocating.

The compiler inlines everything into one tight loop. What looks like 5 chained operations becomes one machine-code loop. ⚡

Iterator Adapters: Your New LEGO Set 🧱

Let me show you the hits:

.map() — Transform Elements

// Process signal samples: convert raw ADC values to voltage
let voltages: Vec<f32> = raw_samples.iter()
    .map(|&sample| sample as f32 * (3.3 / 4096.0))
    .collect();

Vs PHP: Same idea. But Rust's version doesn't copy the whole array first.

.filter() — Keep What Matters

// Keep only strong signals (SNR above threshold)
let strong_signals = signals.iter()
    .filter(|s| s.signal_to_noise > 10.0)
    .collect::<Vec<_>>();

.zip() — Pair Up Two Iterators

This one doesn't exist cleanly in PHP, and it's gorgeous:

let frequencies = vec![98.5, 100.3, 101.1];
let strengths = vec![-45.2, -62.8, -38.1];

// Pair them up, filter weak signals, collect station info
let good_stations: Vec<_> = frequencies.iter()
    .zip(strengths.iter())
    .filter(|(_, &strength)| strength > -50.0)
    .map(|(&freq, &strength)| (freq, strength))
    .collect();
// Only 98.5 MHz and 101.1 MHz make the cut!

For my RF/SDR work: Pairing frequency scans with signal strength readings is a daily task. .zip() makes it feel like magic. 📻

.flat_map() — Flatten as You Go

// Each radio station has multiple sub-channels
let all_channels: Vec<u32> = stations.iter()
    .flat_map(|station| station.channels.iter().copied())
    .collect();

.enumerate() — Index for Free

// Find which sample index has the peak
for (index, &sample) in signal.iter().enumerate() {
    if sample > peak_threshold {
        println!("Peak at sample #{}: {}", index, sample);
    }
}

.take_while() and .skip_while() — Conditional Slicing

// Process signal samples until the signal drops off
let useful_samples: Vec<_> = signal.iter()
    .skip_while(|&&s| s.abs() < noise_floor)  // Skip initial noise
    .take_while(|&&s| s.abs() > noise_floor)  // Take the signal burst
    .collect();

This is what lazy evaluation is MADE for. You never process a single sample beyond what you need. 🎯

The Performance Reality Check 📊

Let me give you numbers that made my jaw drop when I moved from Node.js signal processing to Rust.

Processing 1 million radio samples:

Node.js (eager array methods):     ~180ms   + heavy GC pauses
Python (numpy, for comparison):    ~12ms    (C underneath)
Rust (iterator chain):             ~4ms     (zero allocation!)

The secret sauce: Rust's iterator chains get monomorphized (specialized for your exact types) and inlined by the compiler into one single optimized loop. The layers of .map().filter().take() you wrote? The CPU sees ONE loop with ONE branch.

In Node.js terms: It's like writing .map().filter().reduce() but the JavaScript engine somehow compiles it into a single for loop with no intermediate arrays. Except that's impossible in JS. But Rust does it. Every time. 🤯

Writing Your Own Iterator: Not as Scary as It Sounds 😌

For my RF projects, I needed an iterator that reads chunks of a radio signal file. Here's the stripped-down version:

struct SignalChunks {
    data: Vec<f32>,
    chunk_size: usize,
    position: usize,
}

impl Iterator for SignalChunks {
    type Item = Vec<f32>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.position >= self.data.len() {
            return None;  // Done!
        }
        let end = (self.position + self.chunk_size).min(self.data.len());
        let chunk = self.data[self.position..end].to_vec();
        self.position = end;
        Some(chunk)
    }
}

// And now it works with ALL iterator methods!
let chunks = SignalChunks { data: my_signal, chunk_size: 1024, position: 0 };
let peak_chunks: Vec<_> = chunks
    .filter(|chunk| chunk.iter().any(|&s| s.abs() > threshold))
    .collect();

Once you implement next(), you get map, filter, zip, enumerate, sum, count — all 70+ methods — for free. 🎁

Coming from Laravel: This is like implementing IteratorAggregate and suddenly having the entire Collection API available. Except it's zero-overhead.

Consuming Adapters: Where the Work Actually Happens 🏁

All those .map() and .filter() calls just DESCRIBE what to do. These are what actually DO it:

// .collect() — gather into a collection
let vec: Vec<_> = iter.collect();

// .sum() / .product() — aggregate
let total: f32 = signal.iter().copied().sum();

// .count() — count items
let strong = signals.iter().filter(|s| s.snr > 10.0).count();

// .any() / .all() — early exit checks
let has_interference = signals.iter().any(|s| s.snr < 3.0);

// .find() — get first match (stops early!)
let first_peak = samples.iter().find(|&&s| s > threshold);

// .fold() — custom aggregation (like reduce)
let max_snr = signals.iter()
    .fold(f32::NEG_INFINITY, |max, s| f32::max(max, s.snr));

The .find() one is underrated. It short-circuits! The moment it finds a match, the iterator STOPS. In JavaScript, .find() also stops early — but every .map() or .filter() before it would have processed everything. In Rust, the WHOLE CHAIN stops. 🛑

Why PHP/Laravel Developers Will Get This Instantly 🐘

Laravel Collections are actually a great mental model for Rust iterators. The API is ALMOST identical:

// Laravel
collect($items)
    ->filter(fn($x) => $x > 0)
    ->map(fn($x) => $x * 2)
    ->values()
    ->all();
// Rust
items.iter()
    .filter(|&&x| x > 0)
    .map(|&x| x * 2)
    .collect::<Vec<_>>()

Same shape! But Rust's version:

  • Allocates no intermediate collections
  • Stops early when possible
  • Compiles to a single CPU loop
  • Has zero garbage collection overhead

You already understand the CONCEPT from Laravel. Rust just makes it fast enough for systems work. 🦀

The One Gotcha: Ownership in Iterators ⚠️

Coming from PHP/JS, this tripped me up. There are three flavors:

let data = vec![1, 2, 3, 4, 5];

// .iter()      — borrows, yields &T (read-only references)
for x in data.iter() { println!("{}", x); }  // data still valid!

// .iter_mut()  — borrows mutably, yields &mut T (can modify)
for x in data.iter_mut() { *x *= 2; }  // modifies in place

// .into_iter() — CONSUMES data, yields T (owned values)
for x in data.into_iter() { println!("{}", x); }  // data is GONE

Mental model: iter() is like foreach in PHP (reads without consuming). into_iter() is like a stream that drains the collection. Choose wisely or the borrow checker will choose for you — loudly. 📢

TL;DR: Why Iterators Are Rust's Secret Weapon 🏆

  1. Lazy by default — work only happens when you consume the iterator
  2. Zero-cost composition — chaining 10 adapters = one CPU loop
  3. No intermediate allocations — process millions of items, allocate once
  4. Familiar API — if you know Laravel Collections or JS arrays, you're 80% there
  5. Composable and extensible — implement next(), get 70+ methods free

For my RF/SDR hobby projects, switching from Python/Node.js to Rust iterators was the moment I could ACTUALLY process real-time radio signals on a laptop without losing data. The math on paper said Rust would be faster. The iterator design is WHY it stays fast even when you write beautiful, readable, chained code.

And let's be honest — there's something deeply satisfying about writing:

signals.iter()
    .filter(|s| s.frequency > 88.0 && s.frequency < 108.0)
    .max_by(|a, b| a.snr.partial_cmp(&b.snr).unwrap())

...and KNOWING the CPU is doing exactly one loop, touching exactly the right memory, and stopping the moment it has the answer.

That's what 7 years of PHP couldn't teach me. That's Rust. 🦀⚡


Experimenting with Rust iterators or coming from a Laravel/Node.js background? Hit me up on LinkedIn — always happy to nerd out about zero-cost abstractions!

Want to see iterators in action for real RF signal processing? Check out my GitHub for hobby projects where lazy evaluation is a survival requirement!

Now go iterate lazily. Your CPU will thank you. 🦀🔄