Effectively Using Iterators In Rust
— 3 minutes read
Herman J. Radtke III explains that combinators are only available on iterators. In order to apply combinators on any collection type, an iterator of a specific collection must be created first.
Iter
In Rust, you quickly learn that vector and slice types are not iterable themselves. Depending on which tutorial or example you see first, you call
.iter()
or.into_iter()
. […] Most examples I have found use.iter()
. We can callv.iter()
on something like a vector or slice. This creates anIter<'a, T>
type and it is thisIter<'a, T>
type that implements the Iterator trait and allows us to call functions like.map()
.
It’s important to understand that .iter()
gives references to the collection’s elements. So, if the collection contains references, we end up with references of references:
fn use_names_for_something_else(_names: Vec<&str>) { } fn main() { let names = vec!["Jane", "Jill", "Jack", "John"]; let total_bytes = names .iter() .map(|name: &&str| name.len()) .fold(0, |acc, len| acc + len ); assert_eq!(total_bytes, 16); use_names_for_something_else(names); }
The closure used in
map()
does not require the name parameter to have a type, but I specified the type to show how it is being passed as a reference. Notice that the type of name is&&str
and not&str
. The string “Jane” is of type&str
. Theiter()
function creates an iterator that has a reference to each element in the names vector. Thus, we have a reference to a reference of a string slice. This can get a little unwieldy and I generally do not worry about the type. However, if we are destructuring the type, we do need to specify the reference:fn main() { let player_scores = [ ("Jack", 20), ("Jane", 23), ("Jill", 18), ("John", 19), ]; let players = player_scores .iter() .map(|(player, _score)| { player }) .collect::<Vec<_>>(); assert_eq!(players, ["Jack", "Jane", "Jill", "John"]); }
In the above example, the compiler will complain that we are specifying the type
(_, _)
instead of&(_, _)
. Changing the pattern to&(player, _score)
will satisfy the compiler.
IntoIter
Use the
into_iter()
function when you want to move, instead of borrow, your value.
How for Loops Actually Work
One of the first errors a new Rustacean will run into is the move error after using a for loop:
fn main() { let values = vec![1, 2, 3, 4]; for x in values { println!("{}", x); } let y = values; // move error }
The question we immediately ask ourselves is “How do I create a for loop that uses a reference?”. A for loop in Rust is really just syntatic sugar around
.into_iter().
From the manual:// Rough translation of the iteration without a `for` iterator. let mut it = values.into_iter(); loop { match it.next() { Some(x) => println!("{}", x), None => break, } }
Now that we know
.into_iter()
creates a typeIntoIter<T>
that movesT,
this behavior makes perfect sense. If we want to use values after the for loop, we just need to use a reference instead:fn main() { let values = vec![1, 2, 3, 4]; for x in &values { println!("{}", x); } let y = values; // perfectly valid }
Instead of moving values, which is type
Vec<i32>,
we are moving &values, which is type&Vec<i32>
. The for loop only borrows &values for the duration of the loop and we are able to move values as soon as the for loop is done.
core::iter::Cloned
Sometimes it is necessary to clone certain elements of a collection. For example when a new collection containing filtered elements is required. In order to avoid unnecessary allocations, it’s cheaper to only clone the required elements instead of cloning the original collection and then filtering.
x.clone().into_iter() take(2).collect::<Vec<_>>(); // This is expensive
x.iter().map(|i| i.clone()).take(2).collect::<Vec<_>>(); // This is cheaper
For the second case there is a library function that allows the Rust compiler to optimize allocations for required elements only:
x.iter().cloned().take(2).collect::<Vec<_>>(); // This is the cheaper variant using `cloned()`