Lesson Details

Iterators

When looping over a collection’s items, like a Vec<i32>, iterators are the idiomatic approach. Even a for loop is syntatic sugar for an iterator.

Iterator is a trait, which means there are many possible structs that can implement iterator. For example, Vec and slice have a number of functions that can return iterators. This includes, but is not limited to, iter, windows, extract_if, and more.

iter is fairly common on collections that can be iterated over. It returns an iterator over the items in the collection one by one.

fn main() {
let numbers = vec![10, 20, 30, 40, 50, 60];

for num in numbers.iter() {
println!("{num}");
}
}

The output prints each number in turn.

❯ cargo run
10
20
30
40
50
60

windows is slightly more interesting, the items provided in each iteration are a rolling window, viewed as a slice of arbitrary length.

fn main() {
let numbers = vec![10, 20, 30, 40, 50, 60];

for slice in numbers.windows(2) {
println!("{slice:?}");
}
}

This results in printing pairs of numbers, because we chose a window size of 2.

❯ cargo run
[10, 20]
[20, 30]
[30, 40]
[40, 50]
[50, 60]

A more interesting iterator is provided by extract_if, which accepts a range to operate on, and a closure to test the items. If the item passes the test, it is removed from the original Vec. This leaves all the items that failed the test in the original Vec, and all the items that pass the test become available in the iterator.

fn main() {
let mut numbers: Vec<u32> =
vec![10, 20, 30, 40, 50, 60];

let extracting = numbers.extract_if(.., |item| {
(*item / 10).is_multiple_of(2)
});

for slice in extracting {
println!("{slice:?}");
}

dbg!(numbers);
}

This produces a log for each item before logging the entire original Vec, which has now been mutated.

20
40
60
[src/main.rs:12:5] numbers = [
10,
30,
50,
]

Iterators can also return a single value, such as when using sum to sum all the items, all to check that all items pass a test, or a function like find_map which will find an item that passes a test and transform it into a different type. This can be very useful when dealing with operations that can fail, like parse. Here’s an iterator that finds the first parsable number in a Vec<&str>.

fn main() {
let a = ["tree", "2", "5"];

let first_number = a.iter().find_map(|s| s.parse::<i32>().ok());

dbg!(first_number);
}

Which will find the number 2.

[src/main.rs:7:5] first_number = Some(
2,
)

Iterator Adapters and collect

Once you have an iterator, you can use any of the functions on the Iterator trait to continue to operate on the items. This includes functions like map which can transform one type of value into another, or functions like filter which can filter items out.

Note however that iterators in Rust are lazy and do nothing if they aren’t called. We can call it manually, such as with a for loop, or use a consuming iterator function like collect.

One nice piece of Rust is that if you construct an iterator and don’t call it, the compiler will warn you. This program will produce that warning, and show that the original vec was not modified.

fn main() {
let numbers: Vec<u32> = vec![10, 20, 30, 40, 50, 60];

numbers
.iter()
.filter(|item| (*item / 10).is_multiple_of(2))
.map(|item| item.to_string());

dbg!(numbers);
}

This is the warning:

warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | / numbers
5 | | .iter()
6 | | .filter(|item| (*item / 10).is_multiple_of(2))
7 | | .map(|item| item.to_string());
| |_____________________________________^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default

We can use collect to collect items into a new Vec of the appropriate size.

fn main() {
let numbers: Vec<u32> = vec![10, 20, 30, 40, 50, 60];

let new_numbers: Vec<String> = numbers
.iter()
.filter(|item| (*item / 10).is_multiple_of(2))
.map(|item| item.to_string())
.collect();

dbg!(new_numbers);
}
[src/main.rs:10:5] new_numbers = [
"20",
"40",
"60",
]

Data Parallelism

Rust’s iterators provide a base for more advanced operations. For example, rayon provides data parallelism on iterators.

rayon guarantees data-race free executions and takes advantage of parallelism when sensible, based on work-load at runtime.

To parallelize an iterator, it can be as easy as using par_iter instead of iter.

use rayon::prelude::*;

fn main() {
let numbers: Vec<u32> = vec![10, 20, 30, 40, 50, 60];

let new_numbers: Vec<String> = numbers
.par_iter()
.filter(|item| (*item / 10).is_multiple_of(2))
.map(|item| item.to_string())
.collect();

dbg!(new_numbers);
}