RustArena

Step 6 of 7

Returning iterators

A function that returns a Vec decides for the caller: it computes everything and allocates a new vector, even if the caller only wanted the first match, or a count, or to feed the items into another chain. Returning an iterator hands the caller a lazy sequence instead: nothing runs until they consume it, and they choose how.

struct User {
    name: String,
    active: bool,
    score: u32,
}

struct Team {
    users: Vec<User>,
}

impl Team {
    fn active(&self) -> impl Iterator<Item = &User> {
        self.users.iter().filter(|u| u.active)
    }

    fn names(&self) -> impl Iterator<Item = &str> {
        self.users.iter().map(|u| u.name.as_str())
    }
}

fn total(scores: impl IntoIterator<Item = u32>) -> u32 {
    scores.into_iter().sum()
}

fn main() {
    let team = Team {
        users: vec![
            User { name: String::from("ana"), active: true, score: 70 },
            User { name: String::from("bo"), active: false, score: 95 },
            User { name: String::from("cy"), active: true, score: 40 },
        ],
    };
    let strong: Vec<&str> = team.active().filter(|u| u.score > 50).map(|u| u.name.as_str()).collect();
    println!("{strong:?}");
    println!("{}", team.names().collect::<Vec<_>>().join(", "));
    println!("{}", total(team.active().map(|u| u.score)));
    println!("{}", total(vec![1, 2, 3]));
    println!("{:?}", team.active().next().map(|u| &u.name));
}
["ana"]
ana, bo, cy
110
6
Some("ana")

Piece by piece:

When branches return different iterators

Each adapter chain has its own type, so this doesn't compile:

fn pick(v: &[u32], only_even: bool) -> impl Iterator<Item = &u32> {
    if only_even {
        v.iter().filter(|x| *x % 2 == 0)
    } else {
        v.iter()
    }
}

fn main() {
    println!("{}", pick(&[1, 2, 3], true).count());
}
error[E0308]: `if` and `else` have incompatible types

impl Trait means one concrete type chosen by the function. When it truly varies, return a trait object: Box<dyn Iterator<Item = &u32> + '_>, with Box::new(...) around each branch. It costs one allocation and dynamic calls, which is usually fine.

Common mistakes

Your task

Give Log three methods that return iterators (no Vec in their return types): errors() yields the lines that start with ERROR; with_level(level) yields the message part of the lines that start with exactly that level followed by a space, so with_level("WARN") on WARN disk 91% yields disk 91% (and skips WARNING ...); numbered() yields (line_number, line) pairs counting from 1.

Challenge 2

Infinite iterators are fine when they're lazy. Write fn fibonacci() -> impl Iterator<Item = u64> (0, 1, 1, 2, 3, 5, ...) and fn multiples(n: u32) -> impl Iterator<Item = u32> (n, 2n, 3n, ... forever). The callers decide how many to take. std::iter::successors or a struct implementing Iterator both work.

Challenge 3

Return different iterators from different branches. Write fn select<'a>(items: &'a [Item], filter: Filter) -> Box<dyn Iterator<Item = &'a Item> + 'a> where Filter::All yields every item, Filter::InStock only items with qty > 0, and Filter::Category(name) only items whose category equals name.

Previous: RAII guards and Drop Next: Code review: from working to idiomatic