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.