DaleSchool

Iterators: Looping the Rust Way

Intermediate20min

Learning Objectives

  • Understand the differences between iter(), into_iter(), and iter_mut()
  • Use iterator adapters like map(), filter(), and collect()
  • Refactor for loops into iterator chains
  • Explain that iterators use lazy evaluation

Working Code

In Phase 1 you used for loops to iterate over vectors. Iterators let you do the same work in a different style:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // for loop approach
    let mut doubled_loop = Vec::new();
    for n in &numbers {
        doubled_loop.push(n * 2);
    }

    // Iterator chain approach
    let doubled_iter: Vec<i32> = numbers.iter().map(|n| n * 2).collect();

    println!("for loop: {:?}", doubled_loop);
    println!("Iterator: {:?}", doubled_iter);
    // Both produce [2, 4, 6, 8, 10]
}

Iterator chains express what you want to do declaratively. The closures from Lesson 22 shine here!

Try It Yourself

  1. Run numbers.iter().filter(|n| **n > 3).collect::<Vec<_>>(). What's the result?
  2. Combine map and filter to "double the numbers greater than 3."

iter() vs into_iter() vs iter_mut()

The difference between these three is ownership — the same concept from Phase 2!

| Method | Element type | Original usable? | Analogy | | ------------- | ---------------------- | ---------------- | ----------------------- | | iter() | &T (immutable ref) | Yes | Reading a book | | iter_mut() | &mut T (mutable ref) | Yes (modified) | Writing notes in a book | | into_iter() | T (ownership moves) | No | Taking the book |

fn main() {
    // iter() — borrow and read
    let names = vec!["Alice", "Bob", "Carol"];
    let lengths: Vec<usize> = names.iter().map(|n| n.len()).collect();
    println!("Lengths: {:?}", lengths);
    println!("Original: {:?}", names); // still usable

    // iter_mut() — borrow and modify
    let mut scores = vec![80, 90, 75];
    scores.iter_mut().for_each(|s| *s += 5);
    println!("Adjusted: {:?}", scores); // [85, 95, 80]

    // into_iter() — takes ownership
    let words = vec![String::from("hello"), String::from("world")];
    let upper: Vec<String> = words.into_iter().map(|w| w.to_uppercase()).collect();
    println!("Uppercased: {:?}", upper);
    // println!("{:?}", words); // error! ownership moved
}

Tip: for item in &vec is the same as vec.iter(), and for item in vec is the same as vec.into_iter()!

Key Iterator Adapters

map — Transform

Transform each element into a different value:

fn main() {
    let names = vec!["alice", "bob", "carol"];
    let greeting: Vec<String> = names
        .iter()
        .map(|name| format!("Hello, {name}!"))
        .collect();
    println!("{:?}", greeting);
    // ["Hello, alice!", "Hello, bob!", "Hello, carol!"]
}

filter — Keep Only Matching Elements

Keep only elements that satisfy a condition:

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

enumerate — Pair With Index

Attach an index to each element. Handy when you need a counter in a loop:

fn main() {
    let fruits = vec!["apple", "banana", "grape"];

    // for loop + manual index
    let mut i = 0;
    for fruit in &fruits {
        println!("{i}: {fruit}");
        i += 1;
    }

    // enumerate — much cleaner!
    for (i, fruit) in fruits.iter().enumerate() {
        println!("{i}: {fruit}");
    }
}

zip — Pair Two Collections

Combine two iterators into pairs:

fn main() {
    let names = vec!["Alice", "Bob", "Carol"];
    let scores = vec![95, 87, 92];

    let report: Vec<String> = names
        .iter()
        .zip(scores.iter())
        .map(|(name, score)| format!("{name}: {score}"))
        .collect();

    println!("{:?}", report);
    // ["Alice: 95", "Bob: 87", "Carol: 92"]
}

fold — Reduce to a Single Value

Accumulate all elements into one value. Similar to reduce in other languages:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // Sum
    let sum = numbers.iter().fold(0, |acc, n| acc + n);
    println!("Sum: {sum}"); // 15

    // String concatenation
    let words = vec!["Rust", " is", " great"];
    let sentence = words.iter().fold(String::new(), |mut acc, w| {
        acc.push_str(w);
        acc
    });
    println!("{sentence}"); // Rust is great
}

Tip: For a simple sum, numbers.iter().sum::<i32>() is more concise.

for Loop vs Iterator Chain

Let's compare both approaches on the same task:

Task: From a list of scores, select those >= 60, add a 10-point bonus, and compute the total.

fn main() {
    let scores = vec![45, 82, 67, 93, 55, 78];

    // for loop approach
    let mut total_loop = 0;
    for score in &scores {
        if *score >= 60 {
            total_loop += score + 10;
        }
    }

    // Iterator chain approach
    let total_iter: i32 = scores
        .iter()
        .filter(|s| **s >= 60)
        .map(|s| s + 10)
        .sum();

    println!("for loop: {total_loop}");
    println!("Iterator: {total_iter}");
    // Both produce 350
}

The iterator chain reads like a pipeline: filter (>= 60) -> transform (+10) -> sum.

"Why?" — Lazy Evaluation

Iterator adapters (map, filter, etc.) use lazy evaluation. Nothing runs until you call a consuming adapter like .collect() or .sum().

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // No computation happens here!
    let lazy = numbers.iter().map(|n| {
        println!("Processing: {n}");
        n * 2
    });

    println!("--- before collect ---");

    // Computation starts when collect() is called!
    let result: Vec<&i32> = lazy.collect();
    println!("Result: {:?}", result);
}

Run this and "--- before collect ---" prints first, then the "Processing: ..." lines appear.

This means unnecessary computation is avoided. For example, filter followed by take(3) stops as soon as 3 matching items are found.

Exercise 1 (easy). Rewrite this for loop as an iterator chain.

fn main() {
    let words = vec!["hello", "world", "rust"];

    // Rewrite this as an iterator chain
    let mut uppercased = Vec::new();
    for word in &words {
        uppercased.push(word.to_uppercase());
    }
    println!("{:?}", uppercased);
}

Exercise 2 (medium). Use iterator methods to find the range (max - min) of a score vector.

fn score_range(scores: &[i32]) -> Option<i32> {
    // Complete this
    // Hint: use iter().max() and iter().min()
    // Both return Option<&i32>
}

fn main() {
    let scores = vec![72, 95, 68, 88, 91];
    match score_range(&scores) {
        Some(range) => println!("Score range: {range}"), // 27
        None => println!("No scores"),
    }
}

Exercise 3 (challenge). From a vector of strings, count words with 5+ characters and collect them uppercased into a new vector. Use iterator chains.

fn main() {
    let words = vec!["hi", "hello", "rust", "programming", "code", "iterator"];
    // Count of words with 5+ characters: ???
    // Those words uppercased: ???
}

Q1. What is the difference between iter() and into_iter()?

  • A) iter() is faster
  • B) into_iter() returns immutable references
  • C) iter() borrows for reading; into_iter() takes ownership
  • D) No difference

Q2. What does this code print?

fn main() {
    let nums = vec![1, 2, 3, 4, 5];
    let result: Vec<i32> = nums
        .iter()
        .filter(|n| **n > 2)
        .map(|n| n * 10)
        .collect();
    println!("{:?}", result);
}
  • A) [10, 20, 30, 40, 50]
  • B) [1, 2, 3, 4, 5]
  • C) [30, 40, 50]
  • D) [3, 4, 5]

Q3. What is "lazy evaluation" for iterators?

  • A) Iterators run slowly
  • B) Iterators always compute all elements up front
  • C) Computation only runs when a consuming adapter like collect() is called
  • D) map() and filter() return results immediately