Rust - Iterators
Overview
Estimated time: 45–55 minutes
Master Rust's powerful iterator system. Learn about lazy evaluation, iterator adaptors, consumers, and how to write efficient, functional-style code. Understand the Iterator trait and create custom iterators.
Learning Objectives
- Understand the Iterator trait and lazy evaluation.
- Use iterator adaptors like
map
,filter
, andenumerate
. - Apply iterator consumers like
collect
,fold
, andfor_each
. - Create custom iterators and implement Iterator trait.
- Write performant iterator chains for data processing.
Prerequisites
Iterator Basics
Creating Iterators
Learn different ways to create iterators in Rust:
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// iter() creates an iterator over references
let iter1 = vec.iter();
for item in iter1 {
println!("Reference: {}", item); // item is &i32
}
// into_iter() creates an iterator over owned values
let iter2 = vec.clone().into_iter();
for item in iter2 {
println!("Owned: {}", item); // item is i32
}
// iter_mut() creates an iterator over mutable references
let mut vec_mut = vec![1, 2, 3, 4, 5];
for item in vec_mut.iter_mut() {
*item *= 2; // item is &mut i32
}
println!("Modified: {:?}", vec_mut); // [2, 4, 6, 8, 10]
// Range iterators
for i in 0..5 {
println!("Range: {}", i);
}
// Iterator from function
let iter3 = (0..10).step_by(2);
for item in iter3 {
println!("Step by 2: {}", item); // 0, 2, 4, 6, 8
}
}
Lazy Evaluation
Iterators in Rust are lazy - they do nothing until consumed:
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// This doesn't actually do any work yet!
let iter = vec.iter()
.map(|x| {
println!("Processing: {}", x); // This won't print
x * 2
})
.filter(|&x| x > 4);
println!("Iterator created, but no processing happened yet");
// Only when we consume the iterator does the work happen
let results: Vec = iter.collect();
println!("Results: {:?}", results); // [6, 8, 10]
// Each consumption processes the chain fresh
let vec2 = vec![1, 2, 3, 4, 5];
let processed: Vec = vec2.iter()
.map(|x| x * 2) // [2, 4, 6, 8, 10]
.filter(|&x| x > 4) // [6, 8, 10]
.collect();
println!("Processed: {:?}", processed);
}
Iterator Adaptors
Transforming Data
Use iterator adaptors to transform data:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map - transform each element
let doubled: Vec = numbers.iter()
.map(|x| x * 2)
.collect();
println!("Doubled: {:?}", doubled);
// filter - keep elements that match predicate
let evens: Vec<&i32> = numbers.iter()
.filter(|&x| x % 2 == 0)
.collect();
println!("Evens: {:?}", evens);
// enumerate - add indices
let with_indices: Vec<(usize, &i32)> = numbers.iter()
.enumerate()
.collect();
println!("With indices: {:?}", with_indices);
// zip - combine with another iterator
let letters = vec!['a', 'b', 'c', 'd', 'e'];
let zipped: Vec<(&i32, &char)> = numbers.iter()
.zip(letters.iter())
.collect();
println!("Zipped: {:?}", zipped);
// take and skip
let first_three: Vec<&i32> = numbers.iter().take(3).collect();
let skip_two: Vec<&i32> = numbers.iter().skip(2).collect();
println!("First three: {:?}", first_three); // [1, 2, 3]
println!("Skip two: {:?}", skip_two); // [3, 4, 5, 6, 7, 8, 9, 10]
}
Chaining Operations
Combine multiple iterator adaptors for complex transformations:
fn main() {
let words = vec!["hello", "world", "rust", "programming", "is", "awesome"];
// Complex iterator chain
let result: Vec = words.iter()
.filter(|word| word.len() > 4) // Keep long words
.map(|word| word.to_uppercase()) // Convert to uppercase
.enumerate() // Add indices
.map(|(i, word)| format!("{}. {}", i + 1, word)) // Format with numbers
.collect();
println!("Processed words:");
for item in result {
println!("{}", item);
}
// 1. HELLO
// 2. WORLD
// 3. PROGRAMMING
// 4. AWESOME
// Mathematical operations
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let sum_of_squares: i32 = numbers.iter()
.filter(|&x| x % 2 == 0) // Even numbers only
.map(|x| x * x) // Square them
.sum(); // Sum the results
println!("Sum of squares of even numbers: {}", sum_of_squares); // 220
}
Iterator Consumers
Collecting Results
Use consumers to get final results from iterator chains:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// collect - gather into a collection
let doubled: Vec = numbers.iter().map(|x| x * 2).collect();
let as_string: String = numbers.iter().map(|x| x.to_string()).collect();
println!("Doubled: {:?}", doubled);
println!("As string: {}", as_string); // "12345678910"
// fold and reduce - accumulate values
let sum = numbers.iter().fold(0, |acc, x| acc + x);
let product = numbers.iter().fold(1, |acc, x| acc * x);
println!("Sum: {}, Product: {}", sum, product);
// reduce - like fold but uses first element as initial value
let max = numbers.iter().reduce(|acc, x| if acc > x { acc } else { x });
println!("Max: {:?}", max); // Some(10)
// find - get first matching element
let found = numbers.iter().find(|&x| x > 5);
println!("First > 5: {:?}", found); // Some(6)
// any and all - boolean tests
let has_even = numbers.iter().any(|&x| x % 2 == 0);
let all_positive = numbers.iter().all(|&x| x > 0);
println!("Has even: {}, All positive: {}", has_even, all_positive);
// count - count elements
let even_count = numbers.iter().filter(|&x| x % 2 == 0).count();
println!("Even count: {}", even_count); // 5
}
Side Effects and Consumption
Use consumers that perform side effects:
fn main() {
let names = vec!["Alice", "Bob", "Charlie", "Diana"];
// for_each - perform action on each element
names.iter().for_each(|name| println!("Hello, {}!", name));
// inspect - peek at values during iteration (for debugging)
let processed: Vec = (1..5)
.inspect(|x| println!("Original: {}", x))
.map(|x| x * x)
.inspect(|x| println!("Squared: {}", x))
.filter(|&x| x > 4)
.inspect(|x| println!("Filtered: {}", x))
.collect();
println!("Final result: {:?}", processed);
// partition - split into two collections
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let (evens, odds): (Vec<&i32>, Vec<&i32>) = numbers.iter()
.partition(|&x| x % 2 == 0);
println!("Evens: {:?}", evens); // [2, 4, 6, 8, 10]
println!("Odds: {:?}", odds); // [1, 3, 5, 7, 9]
}
Custom Iterators
Implementing Iterator Trait
Create your own iterator types:
// Counter iterator that counts from start to end
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Counter {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option {
if self.current < self.max {
let current = self.current;
self.current += 1;
Some(current)
} else {
None
}
}
}
// Fibonacci iterator
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Fibonacci {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option {
let current = self.current;
self.current = self.next;
self.next = current + self.next;
Some(current)
}
}
fn main() {
// Use custom counter
let counter = Counter::new(5);
let squares: Vec = counter.map(|x| x * x).collect();
println!("Squares: {:?}", squares); // [0, 1, 4, 9, 16]
// Use Fibonacci iterator
let fib_numbers: Vec = Fibonacci::new().take(10).collect();
println!("Fibonacci: {:?}", fib_numbers);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Combine custom iterators
let combined: Vec<(usize, u64)> = Counter::new(5)
.zip(Fibonacci::new())
.collect();
println!("Combined: {:?}", combined);
// [(0, 0), (1, 1), (2, 1), (3, 2), (4, 3)]
}
Iterator with State
Create more complex stateful iterators:
// Moving average iterator
struct MovingAverage {
data: Vec,
window_size: usize,
position: usize,
}
impl MovingAverage {
fn new(data: Vec, window_size: usize) -> MovingAverage {
MovingAverage {
data,
window_size,
position: 0,
}
}
}
impl Iterator for MovingAverage {
type Item = f64;
fn next(&mut self) -> Option {
if self.position + self.window_size <= self.data.len() {
let window = &self.data[self.position..self.position + self.window_size];
let average = window.iter().sum::() / self.window_size as f64;
self.position += 1;
Some(average)
} else {
None
}
}
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let moving_avg = MovingAverage::new(data, 3);
let averages: Vec = moving_avg.collect();
println!("Moving averages (window=3): {:?}", averages);
// [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
}
Performance Considerations
Iterator vs Loop Performance
Understand when iterators are faster than traditional loops:
fn sum_with_loop(data: &[i32]) -> i32 {
let mut sum = 0;
for &item in data {
sum += item;
}
sum
}
fn sum_with_iterator(data: &[i32]) -> i32 {
data.iter().sum()
}
fn filter_map_loop(data: &[i32]) -> Vec {
let mut result = Vec::new();
for &item in data {
if item % 2 == 0 {
result.push(item * 2);
}
}
result
}
fn filter_map_iterator(data: &[i32]) -> Vec {
data.iter()
.filter(|&x| x % 2 == 0)
.map(|x| x * 2)
.collect()
}
fn main() {
let large_data: Vec = (1..1_000_000).collect();
// Both approaches have similar performance due to Rust's optimizations
let sum1 = sum_with_loop(&large_data);
let sum2 = sum_with_iterator(&large_data);
println!("Loop sum: {}", sum1);
println!("Iterator sum: {}", sum2);
// Iterator version is often more readable and just as fast
let filtered1 = filter_map_loop(&large_data[..100]);
let filtered2 = filter_map_iterator(&large_data[..100]);
println!("Loop result length: {}", filtered1.len());
println!("Iterator result length: {}", filtered2.len());
}
Real-World Examples
Data Processing Pipeline
Process data using iterator chains:
use std::collections::HashMap;
#[derive(Debug)]
struct Sale {
product: String,
amount: f64,
region: String,
}
fn main() {
let sales = vec![
Sale { product: "Laptop".to_string(), amount: 1200.0, region: "North".to_string() },
Sale { product: "Phone".to_string(), amount: 800.0, region: "South".to_string() },
Sale { product: "Laptop".to_string(), amount: 1200.0, region: "North".to_string() },
Sale { product: "Tablet".to_string(), amount: 600.0, region: "East".to_string() },
Sale { product: "Phone".to_string(), amount: 800.0, region: "North".to_string() },
];
// Calculate total sales by product
let mut product_totals: HashMap = HashMap::new();
sales.iter()
.for_each(|sale| {
*product_totals.entry(sale.product.clone()).or_insert(0.0) += sale.amount;
});
println!("Product totals: {:?}", product_totals);
// Find top-selling product
let top_product = product_totals.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap());
if let Some((product, total)) = top_product {
println!("Top product: {} (${:.2})", product, total);
}
// Calculate regional statistics
let regional_stats: HashMap = sales.iter()
.fold(HashMap::new(), |mut acc, sale| {
let entry = acc.entry(sale.region.clone()).or_insert((0, 0.0));
entry.0 += 1; // count
entry.1 += sale.amount; // total
acc
});
println!("Regional statistics:");
for (region, (count, total)) in regional_stats {
println!(" {}: {} sales, ${:.2} total", region, count, total);
}
}
Common Pitfalls
Mistakes to Avoid
- Forgetting lazy evaluation: Iterators do nothing until consumed
- Unnecessary collections: Avoid
collect()
when not needed - Ownership confusion: Understand
iter()
vsinto_iter()
- Performance assumptions: Profile before optimizing
Checks for Understanding
- What's the difference between
iter()
andinto_iter()
? - Why are iterators in Rust called "lazy"?
- What's the difference between
fold()
andreduce()
? - How do you create an infinite iterator that generates random numbers?
Answers
iter()
yields references;into_iter()
yields owned values (consuming the collection)- They don't do any work until consumed by a consumer method
fold()
takes an initial value;reduce()
uses the first element as initial value- Create a struct that implements
Iterator
and generates random numbers innext()