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

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

Checks for Understanding

  1. What's the difference between iter() and into_iter()?
  2. Why are iterators in Rust called "lazy"?
  3. What's the difference between fold() and reduce()?
  4. How do you create an infinite iterator that generates random numbers?
Answers
  1. iter() yields references; into_iter() yields owned values (consuming the collection)
  2. They don't do any work until consumed by a consumer method
  3. fold() takes an initial value; reduce() uses the first element as initial value
  4. Create a struct that implements Iterator and generates random numbers in next()

← PreviousNext →