I used to write Rust like I was still writing C — for loops everywhere, mutable accumulators, index variables. The code worked, but it was loud. Five lines to express what should be one. Then I started using iterator chains, and honestly? I’m never going back.
Iterator chains aren’t just syntactic sugar. They’re often faster than hand-written loops because the compiler can optimize them better. Zero-cost abstractions in action.
The Imperative Way vs The Idiomatic Way
Here’s a task: given a list of strings, find all that start with “error:”, strip the prefix, trim whitespace, and collect the results.
The imperative version:
fn extract_errors_imperative(lines: &[String]) -> Vec<String> {
let mut result = Vec::new();
for line in lines {
if line.starts_with("error:") {
let msg = line.strip_prefix("error:").unwrap();
let trimmed = msg.trim().to_string();
result.push(trimmed);
}
}
result
}
The iterator chain version:
fn extract_errors(lines: &[String]) -> Vec<String> {
lines.iter()
.filter_map(|line| line.strip_prefix("error:"))
.map(|msg| msg.trim().to_string())
.collect()
}
Same result. But the iterator version declares what you want, not how to do it step by step. No mutable accumulator. No temporary variables. Just a pipeline of transformations.
The Core Iterator Methods
You need about 10 methods to handle 95% of cases. Here they are.
map — Transform each element
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
filter — Keep elements matching a predicate
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let evens: Vec<&i32> = numbers.iter().filter(|n| *n % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6, 8]
}
filter_map — Filter and transform in one step
This is one of my favorites. It takes a closure that returns Option<T> — Some keeps the element, None drops it.
fn main() {
let inputs = vec!["42", "hello", "17", "world", "99"];
let numbers: Vec<i32> = inputs.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("{:?}", numbers); // [42, 17, 99]
}
Without filter_map, you’d need a filter then a map — two passes where one suffices.
flat_map — Map and flatten
fn main() {
let sentences = vec!["hello world", "foo bar baz"];
let words: Vec<&str> = sentences.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", words); // ["hello", "world", "foo", "bar", "baz"]
}
fold — Reduce to a single value
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, n| acc + n);
println!("Sum: {}", sum); // 15
let product = numbers.iter().fold(1, |acc, n| acc * n);
println!("Product: {}", product); // 120
// Building a string
let csv = numbers.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
println!("CSV: {}", csv); // "1, 2, 3, 4, 5"
}
enumerate — Get index alongside element
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
for (i, fruit) in fruits.iter().enumerate() {
println!("{}. {}", i + 1, fruit);
}
}
zip — Pair up two iterators
fn main() {
let names = vec!["Alice", "Bob", "Charlie"];
let scores = vec![92, 87, 95];
let results: Vec<_> = names.iter().zip(scores.iter()).collect();
println!("{:?}", results);
// [("Alice", 92), ("Bob", 87), ("Charlie", 95)]
}
take and skip — Slice the iterator
fn main() {
let numbers: Vec<i32> = (1..=100).take(5).collect();
println!("{:?}", numbers); // [1, 2, 3, 4, 5]
let numbers: Vec<i32> = (1..=100).skip(95).collect();
println!("{:?}", numbers); // [96, 97, 98, 99, 100]
}
any and all — Boolean checks
fn main() {
let numbers = vec![2, 4, 6, 8];
let all_even = numbers.iter().all(|n| n % 2 == 0);
let has_negative = numbers.iter().any(|n| *n < 0);
println!("All even: {}, Has negative: {}", all_even, has_negative);
// All even: true, Has negative: false
}
find — First matching element
fn main() {
let numbers = vec![1, 3, 5, 8, 9, 10];
let first_even = numbers.iter().find(|n| *n % 2 == 0);
println!("{:?}", first_even); // Some(8)
}
Chaining Is Where It Gets Powerful
Individual methods are useful. Chains are where iterators truly shine.
#[derive(Debug)]
struct Transaction {
from: String,
to: String,
amount: f64,
}
fn top_recipients(transactions: &[Transaction], n: usize) -> Vec<(String, f64)> {
use std::collections::HashMap;
let mut totals: HashMap<String, f64> = HashMap::new();
for t in transactions {
*totals.entry(t.to.clone()).or_insert(0.0) += t.amount;
}
let mut sorted: Vec<(String, f64)> = totals.into_iter().collect();
sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
sorted.into_iter().take(n).collect()
}
fn main() {
let txns = vec![
Transaction { from: "Alice".into(), to: "Bob".into(), amount: 100.0 },
Transaction { from: "Charlie".into(), to: "Bob".into(), amount: 200.0 },
Transaction { from: "Alice".into(), to: "Dave".into(), amount: 150.0 },
Transaction { from: "Bob".into(), to: "Dave".into(), amount: 50.0 },
];
for (name, total) in top_recipients(&txns, 2) {
println!("{}: ${:.2}", name, total);
}
}
Laziness — Iterators Don’t Execute Until Consumed
This is crucial. Iterator adapters like map, filter, and take are lazy. They don’t do anything until you call a consuming method like collect, for_each, sum, count, or find.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// This does NOTHING — it's lazy
let _lazy = numbers.iter().map(|n| {
println!("Processing {}", n); // Never printed!
n * 2
});
// This actually executes
let doubled: Vec<i32> = numbers.iter().map(|n| {
println!("Processing {}", n); // Printed!
n * 2
}).collect();
println!("{:?}", doubled);
}
This laziness means you can chain a dozen operations and the iterator only makes one pass through the data. No intermediate collections. No wasted allocations.
.iter() vs .into_iter() vs .iter_mut()
This trips everyone up at first.
.iter()— borrows each element: yields&T.iter_mut()— mutably borrows each element: yields&mut T.into_iter()— takes ownership of each element: yieldsT
fn main() {
let names = vec![String::from("Alice"), String::from("Bob")];
// .iter() — borrows. `names` is still valid after.
for name in names.iter() {
println!("{}", name); // name is &String
}
println!("Still have names: {:?}", names);
// .into_iter() — consumes. `names` is gone.
let names = vec![String::from("Alice"), String::from("Bob")];
let upper: Vec<String> = names.into_iter()
.map(|mut n| { n.make_ascii_uppercase(); n })
.collect();
// println!("{:?}", names); // ERROR: names was consumed
println!("{:?}", upper);
}
My rule: use .iter() by default. Use .into_iter() when you want to transform a collection into a new one and don’t need the original. Use .iter_mut() when you need to modify elements in place.
The Turbofish on collect
collect() is generic — it can produce different collection types. You need to tell the compiler which one:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Type annotation on the variable
let set: std::collections::HashSet<&i32> = numbers.iter().collect();
// Or turbofish on collect
let set2 = numbers.iter().collect::<std::collections::HashSet<_>>();
// _ lets the compiler infer the element type
let vec = numbers.iter().map(|n| n * 2).collect::<Vec<_>>();
println!("{:?}", set);
println!("{:?}", vec);
}
I personally prefer the turbofish style — it keeps the type close to where it matters.
When Manual Loops Are Still Better
I’m not a zealot. Some cases genuinely read better as for loops:
Complex control flow with multiple breaks/continues:
fn find_pair(numbers: &[i32], target: i32) -> Option<(i32, i32)> {
for (i, &a) in numbers.iter().enumerate() {
for &b in &numbers[i + 1..] {
if a + b == target {
return Some((a, b));
}
}
}
None
}
When you need mutable state across iterations:
fn running_average(values: &[f64]) -> Vec<f64> {
let mut sum = 0.0;
let mut result = Vec::with_capacity(values.len());
for (i, &v) in values.iter().enumerate() {
sum += v;
result.push(sum / (i + 1) as f64);
}
result
}
Could you write these with scan or fold? Sure. But would they be clearer? Probably not. Use the right tool for the job.
Key Takeaways
- Prefer iterator chains over manual
forloops for data transformations. - Iterators are lazy — nothing happens until a consuming method is called.
filter_mapcombinesfilterandmap.flat_mapcombinesmapandflatten. Use them.- Know the difference between
.iter(),.into_iter(), and.iter_mut(). - Use turbofish (
::<>) or type annotations to tellcollect()what type you want. - Manual loops are fine when they’re clearer — don’t force every loop into an iterator chain.