Closures and Iterators · about 45 min

The Iterator Trait and Adapters

Why this matters

Loops that transform, filter and summarize data are the bread and butter of programming. Rust's iterators let you express them as pipelines: "take the lines, skip blank ones, parse each, keep the positive ones, sum them". The result is often shorter and clearer than the equivalent loop, harder to get wrong (no index bookkeeping), and just as fast: the compiler turns iterator chains into the same machine code as a hand-written loop. Iterators are also how for loops work underneath.

The Iterator trait

An iterator is any type implementing this trait (simplified):

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... plus about 75 provided methods built on next()
}

next returns Some(item) until the sequence is exhausted, then None. type Item is an associated type naming what it yields. A for loop just calls next until None:

fn main() {
    let v = vec![10, 20, 30];
    let mut it = v.iter();
    println!("{:?}", it.next());
    println!("{:?}", it.next());
    println!("{:?}", it.next());
    println!("{:?}", it.next());
}
Some(10)
Some(20)
Some(30)
None

next takes &mut self because it advances the iterator's internal position.

Three ways to iterate a collection

methodyieldscollection afterwards
v.iter()&Tstill usable
v.iter_mut()&mut Tstill usable, possibly modified
v.into_iter()T (owned)consumed

for x in &v, for x in &mut v and for x in v call these three. Ranges, chars(), split_whitespace(), lines(), map keys and values are all iterators too.

Adapters: transforming lazily

Adapters take an iterator and return a new one. They do nothing until something consumes the result:

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6];
    let lazy = v.iter().map(|x| x * 10);
    println!("nothing computed yet");
    let result: Vec<i32> = lazy.filter(|x| x % 20 == 0).collect();
    println!("{:?}", result);
    v.iter().map(|x| x * 2);
}
nothing computed yet
[20, 40, 60]

The compiler warns about the last line: an adapter whose result is never used does nothing at all (unused_must_use: "iterators are lazy and do nothing unless consumed").

The common adapters:

fn main() {
    let words = ["apple", "bob", "kayak", "rust", "level"];
    let nums = [3, 1, 4, 1, 5, 9, 2, 6];

    let palins: Vec<_> = words.iter().filter(|w| w.chars().rev().eq(w.chars())).collect();
    let lens: Vec<_> = words.iter().map(|w| w.len()).collect();
    let indexed: Vec<_> = words.iter().enumerate().skip(1).take(2).collect();
    let pairs: Vec<_> = words.iter().zip(nums.iter()).collect();
    let both: Vec<_> = nums.iter().take(2).chain(nums.iter().rev().take(2)).collect();
    let letters: Vec<char> = words.iter().take(2).flat_map(|w| w.chars()).collect();
    let until_big: Vec<_> = nums.iter().take_while(|&&n| n < 5).collect();
    let windows: Vec<i32> = nums.windows(2).map(|w| w[1] - w[0]).collect();

    println!("{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}", palins, lens, indexed, pairs, both, letters, until_big, windows);
}
["bob", "kayak", "level"]
[5, 3, 5, 4, 5]
[(1, "bob"), (2, "kayak")]
[("apple", 3), ("bob", 1), ("kayak", 4), ("rust", 1), ("level", 5)]
[3, 1, 6, 2]
['a', 'p', 'p', 'l', 'e', 'b', 'o', 'b']
[3, 1, 4, 1]
[-2, 3, -3, 4, 4, -7, 4]

A note on the &&x patterns: nums.iter() yields &i32, and filter passes a reference to each item, so its closure receives &&i32. Patterns like |&&n| peel both layers; |n| **n < 5 does the same.

Consumers: producing a result

Consumers pull items through the chain and produce a final value:

fn main() {
    let nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
    let total: i32 = nums.iter().sum();
    let product: i64 = nums.iter().map(|&x| x as i64).product();
    let count = nums.iter().filter(|&&x| x > 2).count();
    let max = nums.iter().max();
    let min_pos = nums.iter().enumerate().min_by_key(|(_, &v)| v).map(|(i, _)| i);
    let any_big = nums.iter().any(|&x| x > 8);
    let all_pos = nums.iter().all(|&x| x > 0);
    let first_even = nums.iter().find(|&&x| x % 2 == 0);
    let pos = nums.iter().position(|&x| x == 5);
    let joined = nums.iter().map(|n| n.to_string()).collect::<Vec<_>>().join("-");
    let folded = nums.iter().fold(0, |acc, &x| acc * 10 + x);
    println!("{total} {product} {count} {:?} {:?} {any_big} {all_pos} {:?} {:?}", max, min_pos, first_even, pos);
    println!("{joined} {folded}");
}
31 6480 5 Some(9) Some(1) true true Some(4) Some(4)
3-1-4-1-5-9-2-6 31415926

How laziness works, and why it is fast

Each adapter is a small struct wrapping the previous iterator. Calling next on the outermost one calls next on the one inside, and so on: items flow through the whole chain one at a time:

 sum() --next--> Filter --next--> Map --next--> slice::Iter --> [1, 2, 3, 4, 5, 6]
       <-Some(40)-       <-Some(40)-    <-Some(&4)-

No intermediate Vec is created between stages, and stages stop early when the consumer is done. Because every adapter is generic and the closures are known at compile time, the optimizer inlines the whole chain into a single loop. This is the "zero-cost abstraction" promise from module 10 in action.

collect into Result: parsing everything or failing

fn main() {
    let good = "1 2 3";
    let bad = "1 two 3";
    let a: Result<Vec<i32>, _> = good.split(' ').map(|s| s.parse::<i32>()).collect();
    let b: Result<Vec<i32>, _> = bad.split(' ').map(|s| s.parse::<i32>()).collect();
    println!("{:?}\n{:?}", a, b);
}
Ok([1, 2, 3])
Err(ParseIntError { kind: InvalidDigit })

Collecting an iterator of Results into Result<Vec<_>, _> gives Ok(vec) if all succeeded, or the first Err. Combined with ? it makes "parse every line or report the first problem" a one-liner.

Common mistakes

Recap

Going further: read the method list of Iterator in the standard library docs and try scan, step_by, peekable, partition and group-style counting with fold into a BTreeMap.

Exercises

Exercise 1 · write code

An iterator pipeline

Read integers from stdin (one per line). Using a single iterator chain where possible (no explicit for loops), print:

evens squared: [4, 16, 36]
sum of odds: 9
first above 4: Some(5)
all positive: true

for the input 1 2 3 4 5 6 (one per line). Use filter, map, collect, sum, find and all.

File: main.rs

Starting point

use std::io;

fn main() {
    let nums: Vec<i64> = io::stdin()
        .lines()
        .map(|l| l.unwrap().trim().parse().unwrap())
        .collect();
}

Example

input

1
2
3
4
5
6

expected output

evens squared: [4, 16, 36]
sum of odds: 9
first above 4: Some(5)
all positive: true

Example

input

-1
3

expected output

evens squared: []
sum of odds: 2
first above 4: None
all positive: false
Hint 1

nums.iter() yields &i64; filter passes &&i64 to its closure, so write |&&x| x % 2 == 0 or |x| **x % 2 == 0.

Hint 2

.sum() needs to know the result type: let s: i64 = ...sum();

Reference solution
use std::io;

fn main() {
    let nums: Vec<i64> = io::stdin()
        .lines()
        .map(|l| l.unwrap().trim().parse().unwrap())
        .collect();
    let evens_sq: Vec<i64> = nums.iter().filter(|&&x| x % 2 == 0).map(|x| x * x).collect();
    let odd_sum: i64 = nums.iter().filter(|&&x| x % 2 != 0).sum();
    let first = nums.iter().find(|&&x| x > 4);
    let positive = nums.iter().all(|&x| x > 0);
    println!("evens squared: {:?}", evens_sq);
    println!("sum of odds: {}", odd_sum);
    println!("first above 4: {:?}", first);
    println!("all positive: {}", positive);
}

Exercise 2 · write code

Zip and enumerate

The first line of stdin holds names, the second their scores (space separated, same count). Print a ranked table sorted by score descending (ties: keep input order), with 1-based rank, using zip to pair them and enumerate for the rank:

1. cy 91
2. ada 87
3. bob 87

for ada bob cy and 87 87 91.

File: main.rs

Starting point

use std::io::{self, Read};

fn main() {
    let mut input = String::new();
    io::stdin().read_to_string(&mut input).unwrap();
    let mut lines = input.lines();
    let names: Vec<&str> = lines.next().unwrap().split_whitespace().collect();
    let scores: Vec<u32> = lines.next().unwrap().split_whitespace().map(|s| s.parse().unwrap()).collect();
}

Example

input

ada bob cy
87 87 91

expected output

1. cy 91
2. ada 87
3. bob 87
Hint 1

names.iter().copied().zip(scores.iter().copied()) yields (&str, u32) pairs; collect them into a Vec.

Hint 2

sort_by is stable, so equal scores keep input order. Then .iter().enumerate() gives 0-based ranks.

Reference solution
use std::io::{self, Read};

fn main() {
    let mut input = String::new();
    io::stdin().read_to_string(&mut input).unwrap();
    let mut lines = input.lines();
    let names: Vec<&str> = lines.next().unwrap().split_whitespace().collect();
    let scores: Vec<u32> = lines.next().unwrap().split_whitespace().map(|s| s.parse().unwrap()).collect();
    let mut pairs: Vec<(&str, u32)> = names.iter().copied().zip(scores.iter().copied()).collect();
    pairs.sort_by(|a, b| b.1.cmp(&a.1));
    for (i, (name, score)) in pairs.iter().enumerate() {
        println!("{}. {} {}", i + 1, name, score);
    }
}

Exercise 3 · quiz

Lazy iterators

What does this program print?

fn main() {
    let v = vec![1, 2, 3];
    let it = v.iter().map(|x| {
        println!("mapping {x}");
        x * 10
    });
    println!("before");
    let total: i32 = it.take(2).sum();
    println!("total {total}");
}
Answer
  • before, mapping 1, mapping 2, total 30

Adapters like map are lazy: creating the chain runs nothing. Work happens only when a consumer (sum) pulls items with next(), one at a time through the whole chain. take(2) stops asking after two items, so the third element is never mapped. Laziness is why iterator chains do no unnecessary work and allocate nothing in between.