RustArena

Step 2 of 7

From loops to iterator chains

Here is the same job done twice: add up the squares of the even numbers.

fn main() {
    let nums = vec![3, 4, 7, 10, 12];

    // A loop: say HOW, step by step
    let mut total = 0;
    for n in &nums {
        if n % 2 == 0 {
            total += n * n;
        }
    }
    println!("loop:  {total}");

    // An iterator chain: say WHAT you want
    let total: i32 = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).sum();
    println!("chain: {total}");
}
loop:  260
chain: 260

Read the chain from left to right:

  1. nums.iter() produces &i32 items.
  2. .filter(|n| ...) is an adapter: it keeps only the items for which the closure returns true. filter hands the closure a reference to each item, here &&i32 (a reference to a reference), so *n gets back to the &i32, which % works with.
  3. .map(|n| n * n) is an adapter that turns each item into something else.
  4. .sum() is a consumer: it pulls every item through the chain and adds them up. It can produce several number types, so the result needs a type (let total: i32).

Adapters (map, filter, and many more) take an iterator and return a new iterator. Consumers (sum, count, collect, ...) run the chain and produce a final value. Every chain has the shape source, adapters, one consumer.

Why professionals prefer chains

Loops are still the right tool when the body does things (printing, writing files) or has tricky control flow with several breaks and returns. Good Rust uses both; most data transformation is written as chains.

collect: build a collection

collect() gathers the items into a collection. Tell it which one with a type annotation:

fn main() {
    let words = vec!["apple", "kiwi", "banana", "fig"];
    let long: Vec<&str> = words.iter().filter(|w| w.len() > 3).copied().collect();
    let shouting: Vec<String> = words.iter().map(|w| w.to_uppercase()).collect();
    println!("{:?}", long);
    println!("{:?}", shouting);
}
["apple", "kiwi", "banana"]
["APPLE", "KIWI", "BANANA", "FIG"]

words.iter() yields &&str (references to the &strs in the vector). .copied() copies each item out of its reference, giving plain &strs to collect. For types that aren't Copy, like String, the same adapter is .cloned().

Common mistakes

fn main() {
    let nums: Vec<i32> = vec![1, 2, 3, 4];
    let evens: Vec<&i32> = nums.iter().filter(|x| x % 2 == 0).collect();
    println!("{:?}", evens);
}
error[E0369]: cannot calculate the remainder of `&&i32` divided by `{integer}`

Dereference: |x| *x % 2 == 0, or take the item apart in the pattern: |&&x| x % 2 == 0.

Your task

Write fn even_squares(nums: &[i32]) -> Vec<i32> that returns the squares of the even numbers, in their original order, using an iterator chain (no loops). For example even_squares(&[1, 2, 3, 4]) is [4, 16].

Challenge 2

Write fn shout_long_words(words: &[&str], min_chars: usize) -> Vec<String> that returns the words with at least min_chars characters, uppercased, in their original order. Count characters, not bytes ("café" has 4 characters but 5 bytes). No loops.

Challenge 3

A bank account's transactions arrive as input, one whole number per line: positive numbers are deposits, negative numbers are withdrawals. Print three lines:

deposits: 350
withdrawals: 50
balance: 300

(withdrawals is the total amount taken out, as a positive number.) The starter reads the numbers into a Vec<i64>. Compute the totals with iterator chains, no loops.

Previous: What an iterator is Next: Laziness: nothing happens until you ask