Frontend Labs
⋯

21. Iterators: lazy, composable, zero-cost

21.1 The trait

// fragment: the real definition, minus ~75 provided methods
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

You implement next. Every adapter (map, filter, zip, take, chain, flat_map, fold, sum, ...) comes for free.

21.2 iter, iter_mut, into_iter

Call Yields The collection afterwards
v.iter() / for x in &v &T unchanged
v.iter_mut() / for x in &mut v &mut T modified in place
v.into_iter() / for x in v T (owned) consumed

21.3 Adapters are lazy

#[derive(Debug)]
struct LogLine { status: u16, ms: u32, path: &'static str }

fn main() {
    let logs = [
        LogLine { status: 200, ms: 12, path: "/" },
        LogLine { status: 500, ms: 340, path: "/api/pay" },
        LogLine { status: 200, ms: 95, path: "/api/cart" },
        LogLine { status: 503, ms: 1200, path: "/api/pay" },
    ];

    // Nothing runs until a CONSUMER (sum, collect, for, count...) pulls values through.
    let error_rate = logs.iter().filter(|l| l.status >= 500).count() as f64 / logs.len() as f64;
    let slow_paths: Vec<&str> = logs.iter().filter(|l| l.ms > 100).map(|l| l.path).collect();
    let p_max = logs.iter().map(|l| l.ms).max().unwrap_or(0);
    let total_ok_ms: u32 = logs.iter().filter(|l| l.status < 400).map(|l| l.ms).sum();

    println!("error rate {:.0}%, slow {slow_paths:?}, max {p_max}ms, ok total {total_ok_ms}ms", error_rate * 100.0);

    // collect() is driven by the TARGET type: Result<Vec<_>, _> stops at the first error.
    let parsed: Result<Vec<u16>, _> = ["80", "443", "x"].iter().map(|s| s.parse::<u16>()).collect();
    println!("{parsed:?}"); // Err(ParseIntError { kind: InvalidDigit })
}

Across the series: this is the error-rate calculation behind the SLIs in HTTPS Ch 23.

21.4 Writing your own iterator

/// Splits a byte buffer into length-prefixed frames: [len:u8][payload...]...
struct Frames<'a> { buf: &'a [u8] }

impl<'a> Iterator for Frames<'a> {
    type Item = Result<&'a [u8], &'static str>;
    fn next(&mut self) -> Option<Self::Item> {
        let (&len, rest) = self.buf.split_first()?;     // None at end of input
        let len = len as usize;
        if rest.len() < len {
            self.buf = &[];                              // stop after reporting the error
            return Some(Err("truncated frame"));
        }
        let (frame, rest) = rest.split_at(len);
        self.buf = rest;
        Some(Ok(frame))
    }
}

fn main() {
    let wire = [3, b'a', b'b', b'c', 0, 2, b'h', b'i', 9, b'x'];
    for f in (Frames { buf: &wire }) {
        println!("{:?}", f.map(|b| String::from_utf8_lossy(b).into_owned()));
    }
}

The same framing problem appears in Go §34 and in Ch 32 over a real socket.

21.5 Zero cost

logs.iter().filter(..).map(..).sum() compiles to the same machine code as a hand-written for loop with an if. Adapters are generic structs (Filter<Map<Iter<..>>>), and after inlining LLVM sees only the loop. Iterators also avoid bounds checks, because the iterator already knows where the slice ends. When performance matters, prefer iterators to manual indexing.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36