The iterator pattern allows us to use functional programming techniques to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don't have to re-implement that logic yourself!
a functional technique
Consider these two implementations of a function that searches for substrings in a list of strings. The first approach is a traditional looping method, the second is a functional approach using Iterators.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
The functional programming style prefers to minimize the amount of mutable state to make code clearer. Removing the mutable state might enable a future enhancement to make searching happen in parallel, because we wouldn’t have to manage concurrent access to the results vector.
However, are we giving up performance for this more expressive style?
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
Nope! Iterators, although a high-level abstraction, get compiled down to roughly the same code as if you’d written the lower-level code yourself. Iterators are one of Rust’s zero-cost abstractions, by which we mean that using the abstraction imposes no additional runtime overhead.
common methods
filter
// Imperitive solution
fn count_for(map: &HashMap<String, Progress>, value: Progress) -> usize {
let mut count = 0;
for val in map.values() {
if *val == value {
count += 1;
}
}
count
}
// Functional solution
fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize {
map.iter().filter(|(_, &val)| val == value).count()
}
fold
Fold lets you increase a counter based on running some function in each iteration. This example takes a slice of the hashmaps from the filter example and calls the count_iterator function on each one, tallying up the total.
fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
collection
.iter()
.fold(0, |acc, map| count_iterator(map, value) + acc)
}
map
The map method lets us run an arbitrary function on each iteration, creating a new iterator with the results. It's very general and very useful, but as an example here's an alternative solution to the fold example above:
fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
collection
.iter()
.map(|map| count_iterator(map, value))
.sum()
}
collect
count
lazy
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
In Rust, iterators are lazy, meaning they have no effect until you call methods
that consume the iterator to use it up. For example, the code above creates an
iterator over the items in the vector v1 by calling the iter method defined on
Vec<T>. This code by itself doesn’t do anything useful.
for val in v1_iter {
println!("Got: {val}");
}
Now we're actually iterating over the values in v1. In languages that don’t have iterators provided by their standard libraries, you would likely write this same functionality by starting a variable at index 0, incrementing it with each loop until it reaches the length of the total number of items in the vector.
.collect()
The collect method lets you iterate through an interator and transform it into a collection.
let list = vec![1, 2, 3];
let doubled: Vec<i32> = list.iter().map(|&x| x * 2).collect();
println!("{doubled:?}");
It's powerful to the point of feeling almost magical:
fn result_with_list() -> Result<Vec<i64>, DivisionError> {
let numbers = [27, 297, 38502, 81];
numbers.into_iter().map(|n| divide(n, 27)).collect()
}
fn list_of_results() -> Vec<Result<i64, DivisionError>> {
let numbers = [27, 297, 38502, 81];
numbers.into_iter().map(|n| divide(n, 27)).collect()
}
Iterator Trait and next Method
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
The Iterator trait only requires implementors to define one method: the next method, which returns one item of the iterator at a time wrapped in Some and, when iteration is over, returns None.
We can call the next method on iterators directly, however in doing so you will
need to make the iterator mutable as calling next directly changes some internal
state on the iterator. When using a for loop the loop takes ownership of the
iterator and makes it mutable behind the scenes.
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
Owned iterator vs immutable references
The iter method produces an iterator over immutable references. If we want to create an iterator that takes ownership of v1 and returns owned values, we can call into_iter instead of iter.