Intermediate Level

Iterators and Closures

Chapter 15: Iterators šŸ”

Process sequences of data efficiently with iterators in Rust.

Understanding Iterators

What are Iterators?

Iterators are objects that can traverse through a collection of items one at a time. In Rust, iterators are lazy, meaning they don't do anything until you call methods that consume or transform them.

fn main() {
    let v1 = vec![1, 2, 3];
    
    // Creating an iterator doesn't execute anything
    let v1_iter = v1.iter();
    
    // Consuming the iterator executes the iteration
    for val in v1_iter {
        println!("Got: {}", val);
    }
}

Iterator Methods

Iterator Adapters

Iterator adapters are methods that produce new iterators:

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];
    
    // map creates a new iterator
    let v2: Vec<i32> = v1.iter().map(|x| x + 1).collect();
    
    println!("{:?}", v2); // [2, 3, 4]
}

Consuming Adapters

Consuming adapters use up the iterator:

fn main() {
    let v1 = vec![1, 2, 3];
    
    // sum consumes the iterator
    let total: i32 = v1.iter().sum();
    
    println!("Total: {}", total); // 6
}

Common Iterator Methods

filter

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    let even_numbers: Vec<i32> = numbers
        .into_iter()
        .filter(|x| x % 2 == 0)
        .collect();
    
    println!("{:?}", even_numbers); // [2, 4, 6, 8, 10]
}

map

fn main() {
    let numbers = vec![1, 2, 3];
    
    let squared: Vec<i32> = numbers
        .iter()
        .map(|x| x * x)
        .collect();
    
    println!("{:?}", squared); // [1, 4, 9]
}

fold/reduce

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // fold with initial value
    let sum = numbers.iter().fold(0, |acc, x| acc + x);
    
    // reduce (similar to fold but without initial value)
    let product = numbers.iter().fold(1, |acc, x| acc * x);
    
    println!("Sum: {}", sum);      // 15
    println!("Product: {}", product); // 120
}

Iterator Types

iter(), into_iter(), and iter_mut()

fn main() {
    let mut numbers = vec![1, 2, 3];
    
    // iter() - borrows each element
    for num in numbers.iter() {
        println!("{}", num);
    }
    
    // into_iter() - takes ownership of each element
    for num in numbers.into_iter() {
        println!("{}", num);
    }
    
    // iter_mut() - mutably borrows each element
    let mut numbers = vec![1, 2, 3];
    for num in numbers.iter_mut() {
        *num *= 2;
    }
    println!("{:?}", numbers); // [2, 4, 6]
}

Chaining Iterator Methods

Method Chaining

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    let result: Vec<i32> = numbers
        .iter()
        .filter(|&x| x % 2 == 0)
        .map(|x| x * x)
        .collect();
    
    println!("{:?}", result); // [4, 16, 36, 64, 100]
}

Practical Examples

Example 1: Processing User Data

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    active: bool,
}

fn main() {
    let users = vec![
        User { name: String::from("Alice"), age: 25, active: true },
        User { name: String::from("Bob"), age: 30, active: false },
        User { name: String::from("Charlie"), age: 35, active: true },
        User { name: String::from("Diana"), age: 28, active: true },
    ];
    
    // Get names of active users over 30
    let active_senior_names: Vec<&String> = users
        .iter()
        .filter(|user| user.active)
        .filter(|user| user.age > 30)
        .map(|user| &user.name)
        .collect();
    
    println!("{:?}", active_senior_names); // ["Charlie"]
    
    // Calculate average age of active users
    let active_users: Vec<&User> = users.iter().filter(|user| user.active).collect();
    let total_age: u32 = active_users.iter().map(|user| user.age).sum();
    let average_age = total_age as f64 / active_users.len() as f64;
    
    println!("Average age of active users: {:.1}", average_age); // 29.3
}

Example 2: Text Processing with Iterators

fn main() {
    let text = "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.";
    
    // Count words
    let word_count = text.split_whitespace().count();
    println!("Word count: {}", word_count);
    
    // Find words longer than 5 characters
    let long_words: Vec<&str> = text
        .split_whitespace()
        .filter(|word| word.len() > 5)
        .collect();
    
    println!("Long words: {:?}", long_words);
    
    // Convert to uppercase and collect
    let uppercase_words: Vec<String> = text
        .split_whitespace()
        .map(|word| word.to_uppercase())
        .collect();
    
    println!("{:?}", uppercase_words);
    
    // Chain multiple operations
    let processed_text: String = text
        .split_whitespace()
        .map(|word| word.to_lowercase())
        .filter(|word| !word.starts_with("a"))
        .collect::<Vec<String>>()
        .join(" ");
    
    println!("Processed text: {}", processed_text);
}

Performance Benefits

Zero-Cost Abstractions

Iterator adapters are zero-cost abstractions, meaning they don't impose any runtime overhead:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Iterator approach (compiled to be as fast as loop approach)
    let sum: i32 = numbers.iter().sum();
    
    // Loop approach (manual implementation)
    let mut total = 0;
    for num in &numbers {
        total += num;
    }
    
    assert_eq!(sum, total);
}

Lazy Evaluation

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // This iterator chain is not executed until collect() is called
    let processed: Vec<i32> = numbers
        .iter()
        .filter(|&x| x % 2 == 0)
        .map(|x| x * x)
        .filter(|&x| x > 10)
        .collect();
    
    println!("{:?}", processed); // [16]
}

Custom Iterators

Implementing the Iterator Trait

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter::new();
    
    for number in counter {
        println!("{}", number);
    }
    
    // Using iterator methods
    let sum: u32 = Counter::new()
        .zip(Counter::new().skip(1))
        .map(|(a, b)| a * b)
        .take(3)
        .sum();
    
    println!("Sum: {}", sum); // 56 (2*3 + 4*5 + 6*7)
}

Common Iterator Methods

find

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    let found = numbers.iter().find(|&x| x == 3);
    
    match found {
        Some(value) => println!("Found: {}", value),
        None => println!("Not found"),
    }
}

any/all

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    let has_even = numbers.iter().any(|&x| x % 2 == 0);
    let all_positive = numbers.iter().all(|&x| x > 0);
    
    println!("Has even number: {}", has_even);     // true
    println!("All positive: {}", all_positive);     // true
}

enumerate

fn main() {
    let words = vec!["Hello", "World", "Rust"];
    
    for (index, word) in words.iter().enumerate() {
        println!("{}: {}", index, word);
    }
}

zip

fn main() {
    let numbers = vec![1, 2, 3];
    let words = vec!["one", "two", "three"];
    
    for (num, word) in numbers.iter().zip(words.iter()) {
        println!("{}: {}", num, word);
    }
}

Common Mistakes

āŒ Not Collecting Iterator Results

fn main() {
    let numbers = vec![1, 2, 3];
    
    // This doesn't actually do anything because iterators are lazy
    let squared: Vec<i32> = numbers.iter().map(|x| x * x);
    // Error: expected Vec<i32>, found Map<..., ...>
    
    // Need to collect the results
    let squared: Vec<i32> = numbers.iter().map(|x| x * x).collect();
}

āœ… Properly Collecting Iterator Results

fn main() {
    let numbers = vec![1, 2, 3];
    
    let squared: Vec<i32> = numbers.iter().map(|x| x * x).collect();
    
    println!("{:?}", squared); // [1, 4, 9]
}

āŒ Moving Values When You Don't Intend To

fn main() {
    let names = vec![String::from("Alice"), String::from("Bob")];
    
    // This moves the strings out of the vector
    for name in names.into_iter() {
        println!("{}", name);
    }
    
    // This won't compile because names has been moved
    // println!("{:?}", names);
}

āœ… Borrowing When You Want to Keep the Data

fn main() {
    let names = vec![String::from("Alice"), String::from("Bob")];
    
    // This borrows the strings
    for name in names.iter() {
        println!("{}", name);
    }
    
    // This works because we only borrowed
    println!("{:?}", names);
}

Key Takeaways

  • āœ… Iterators are lazy and don't execute until consumed
  • āœ… Iterator adapters (map, filter, etc.) don't modify the original collection
  • āœ… Consuming adapters (collect, sum, etc.) actually execute the iteration
  • āœ… Method chaining allows for expressive data processing pipelines
  • āœ… Iterators are zero-cost abstractions with no runtime overhead
  • āœ… Use iter(), into_iter(), and iter_mut() appropriately
  • āœ… Custom iterators can be created by implementing the Iterator trait
  • āœ… Follow Rust naming conventions and idioms for iterator usage

Ready for Chapter 16? → Smart Pointers

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source