RustArena

Step 4 of 6

Iterator chains and avoiding intermediate collections

Iterators are a zero-cost abstraction: in a release build, a chain like .filter().map().sum() compiles to the same machine code as a hand-written loop, often better, because iterators don't need bounds checks on every element. You can't beat them by writing the loop yourself.

What does cost is collecting in the middle. Every .collect::<Vec<_>>() allocates a whole new vector, and the next step then reads it all again:

fn long_words_wasteful(text: &str) -> usize {
    let words: Vec<&str> = text.split_whitespace().collect();
    let long: Vec<&&str> = words.iter().filter(|w| w.len() > 3).collect();
    long.len()
}

fn long_words(text: &str) -> usize {
    text.split_whitespace().filter(|w| w.len() > 3).count()
}

fn main() {
    let text = "the crab scuttled across the warm sand";
    println!("{} {}", long_words_wasteful(text), long_words(text));
}
5 5

Same answer, but the first version builds two vectors it immediately throws away. The chain walks the words once and allocates nothing. clippy's needless_collect lint (in the opt-in clippy::nursery group) points these out.

Laziness lets you stop early

Adapters do nothing until a consumer pulls items, and consumers like any, all, find and position stop as soon as they have the answer (so does any chain with take(n) in it, after n items):

fn main() {
    let mut checked = 0;
    let readings = [12, 15, 99, 14, 13, 18, 11];
    let alarm = readings.iter().any(|&r| {
        checked += 1;
        r > 90
    });
    println!("alarm: {alarm}, readings checked: {checked}");
}
alarm: true, readings checked: 3

If the readings came from a huge file or a network stream, the lazy version stops reading at the third one. Collecting first would read everything.

Don't sort to find one thing

Sorting is O(n log n) and usually needs a copy. To find the largest or smallest item, max, min, max_by_key and min_by_key do one O(n) pass and return a reference, no copy. (For the top k of many, select_nth_unstable beats a full sort.)

Common mistakes

Your task

paid_revenue collects two throwaway vectors and then loops. Rewrite it as one iterator chain: keep paid orders, turn each into qty * price_cents (as u64), and sum them. No collect and no loops.

Challenge 2

first_bad_line receives lines from a stream (it could be a multi-gigabyte log) and returns the position of the first line that isn't a valid integer (after trimming). The current version collects every line before looking. Make it stop as soon as it finds the answer, using position. A hidden test feeds it a stream that panics if read past the bad line.

Challenge 3

top_scorer copies and sorts the whole roster just to read one player. Rewrite it to return Option<&Player> (a reference into the slice) in a single pass, with no sorting, cloning or collecting. If several players share the top score, return the first of them in the list. Hint: max_by_key returns the last of equal maximums, so use reduce (or a loop) that only replaces the best when a score is strictly greater.

Previous: Allocate less Next: Cow: borrow when you can, own when you must