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:
nums.iter()produces&i32items..filter(|n| ...)is an adapter: it keeps only the items for which the closure returnstrue.filterhands the closure a reference to each item, here&&i32(a reference to a reference), so*ngets back to the&i32, which%works with..map(|n| n * n)is an adapter that turns each item into something else..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
- The intent is visible. "Keep the evens, square them, sum" reads like the spec. There's no mutable accumulator to get wrong and no index to go out of bounds.
- Zero cost. The compiler turns the chain into the same machine code as the hand-written loop, sometimes better (no bounds checks).
- Easy to change. Adding a step is adding a line.
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
- Forgetting that
filterpasses a reference to the item:
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.
- A chain with no consumer does nothing at all. That's the next step.
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