cover image by Karen Rustad Tölva.
The answer is easy if you
Take it logically
I'd like to help you in your struggle
To be free
There must be fifty ways
To leave your lover
That was Paul Simon on lovers. This is me on Rust iterators (yeah, living the life). Maybe not 50, but there are a lot of ways to handle your iterator in Rust. And they are equally easy if you take them logically.
Iterator, IntoInterator, and FromIterator traits
There are three main traits that we should look into to grasp iterators in Rust.
The Iterator trait requires the implementation of a single method called next()
, which, when called, returns an Option<Item>
.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Calling next()
will return Some(Item)
as long as there are elements, and once they’ve all been exhausted, it will return None
to indicate that iteration is finished.
In the code below, there is an implementation of the Iterator
trait for a certain Counter
type (the example is from the Introduction to Programming Using Rust book).
struct Counter {
max: i32,
// `count` tracks the state of this iterator
count: i32,
}
impl Counter {
fn new(max: i32) -> Counter {
Counter { count: -1, max: max }
}
}
impl Iterator for Counter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < self.max {
Some(self.count)
} else {
None
}
}
}
for i in Counter::new(10) {
println!("{}", i);
}
The IntoIterator trait requires the implementation of an into_iter()
method. This method defines how any data type can be converted into an interator.
pub trait IntoIterator {
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}
Finally, the FromIterator trait, obviously, takes an iterator and returns a collection.
pub trait FromIterator<A> {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = A>;
}
The synergy between these traits allows an important flexibility when getting a custom type to be iterable. A developer can, of course, implement the Iterator
trait for the type and be done with it. But they can also cheat a bit, in a sense, by implementing the IntoIterator
and FromIterator
traits to produce an existing iterable type, such as a Vec
and offload the iteration responsibility to this intermediate data type.
Let's look into how the above example for the Counter
type could have been implemented this way by using Range from the standard library (again, the example is from the Introduction to Programming Using Rust book).
struct Counter {
max: i32,
// No need to track the state,
// because this isn't an iterator.
}
impl Counter {
fn new(max: i32) -> Counter {
Counter { max: max }
}
}
impl IntoIterator for Counter {
type Item = i32;
type IntoIter = std::ops::Range<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
std::ops::Range{ start: 0, end: self.max }
}
}
for i in Counter::new(10) {
println!("{}", i);
}
into_iter, iter and iter_mut methods
We have already seen that the IntoIterator
trait has only one required method, into_iter()
which converts the thing implementing IntoIterator
into, well, an iterator.
Additionally, collection types generally offer methods that provide iterators over references, called iter()
and iter_mut()
. All these do is borrow either an immutable or mutable reference to the collection and turn it into an iterator.
In other words, if you're confused when to use which, remember that the king is into_iter()
, and iter()
and iter_mut()
are just wrapper methods to borrow and into_iter in one go.
// assuming lovers is Vec<Lover>
// Iter<Vec<Lover>>
// because: impl<'a, T> IntoIterator for Vec<T>
lovers.into_iter()
// Iter<&Vec<Lover>>
// because: impl<'a, T> IntoIterator for &'a Vec<T>
lovers.iter()
// Iter<&mut Vec<Lover>>
// because: impl<'a, T> IntoIterator for &'a mut Vec<T>
lovers.iter_mut()
for loops
Rust's for
loop is just syntactic sugar for iterators.
So this:
// assuming lovers is Vec<Lover>
for lover in lovers {
println!("{}", lover);
}
is actually this:
// assuming lovers is Vec<Lover>
{
let struggle = match IntoIterator::into_iter(lovers) {
mut iter => loop {
let next;
match iter.next() {
// that's what she said
Some(lover) => next = lover,
// said no one ever
None => break,
};
let lover = next;
let () = { println!("{}", lover); };
},
};
struggle
}
As we have seen, into_iter()
takes self, using a for loop to iterate over a collection consumes that collection. Yet, it's possible to loop with for
without consuming. Remember, for a Vec
, the IntoIterator
trait is implemented multiple times.
impl<T> IntoIterator for Vec<T>
impl<'a, T> IntoIterator for &'a Vec<T>
impl<'a, T> IntoIterator for &'a mut Vec<T>
Basically, if we loop over a vector directly, we'll consume it and get owned vector items within the loop. But if we loop over an immutable or mutable (& and &mut) reference to a vector, we'll get an immutable or mutable reference to vector items within the loop.
// assuming lovers is Vec<Lover>
// consuming lovers, lover is an owned Lover
// ^_^ (see what I did there)
for lover in lovers {}
// lover is &Lover
// (because you can't own a lover)
for lover in &lovers {}
// lover is &mut Lover
// (but people don't change)
for lover in &mut lovers {}
Lazy Iterators
As a final note, in Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator.
Methods for consuming an iterator may be defined on the Iterator
trait and are called consuming adapters because they fulfill whatever their purpose is by consuming the iterator via calling next()
on it. Methods such as reduce()
, find()
, sum()
, or collect()
are consuming adapters.
On the other hand, certain methods called iterator adapters may also be defined on the Iterator
trait. These take an iterator and return another. These methods are called between the creation of an iterator and its consumption. The primary example is, of course, map()
. As a result of laziness, map()
will do nothing unless fed into a consuming adapter.
Top comments (0)