4 February 2023
In Rust, iterators are used to process sequences or streams of data. Rust programmers tend to use iterators a lot, so it’s worth taking a close look at them. The basic Iterator trait is not too complicated if we leave out methods with a default implementation:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// skipping 74 methods with default implementations
}
An iterator has a type for the elements it returns and a next
method that returns the
next element, or None to signal that there are no more items. We can create an iterator from a
vector and call next
explicitly:
let v = vec![11, 22, 33];
let mut i = v.iter();
println!("{:?}", i.next()); // prints Some(11)
println!("{:?}", i.next()); // prints Some(22)
println!("{:?}", i.next()); // prints Some(33)
println!("{:?}", i.next()); // prints None
Rust’s for
loop is basically syntactic sugar for working with iterators.
let mut i = v.iter();
for x in i {
println!("{}", x);
}
Vec actually has three methods that create iterators: iter
, iter_mut
, and
into_iter
. The iter
method lets you access items by reference:
let v = vec![11, 22, 33];
for x in v.iter() {
println!("{}", x); // prints 11, then 22, then 33
}
println!("{:?}", v); // [11, 22, 33]
The iter_mut
method gives you mutable references:
let mut v = vec![11, 22, 33];
for x in v.iter_mut() {
*x += 1;
}
println!("{:?}", v); // [12, 23, 34]
Finally, into_iter
takes ownership of the vector and gives you owned items:
let v = vec![11, 22, 33];
for x in v.into_iter() {
println!("{}", x) // prints 11, then 22, then 33;
}
//println!("{:?}", v); // not allowed -- v has been moved
In most cases you don’t have to call those methods directly because there’s a shorthand for each:
for x in v.iter() => for x in &v
for x in v.iter_mut() => for x in &mut v
for x in v.into_iter() => for x in v
I’ve been using Vec in all the examples so far, but other collection types have very similar functionality. For example, HashMap has the same three methods.
The Range type implements Iterator directly. I’ve already used ranges in Rust: Slices, but I didn’t call attention to them then. Here’s a simple example:
let r = Range { start: 1, end: 4 };
for x in r {
println!("{}", x); // prints 1, then 2, then 3
}
Ranges are usually defined with “range expressions”, special syntax built into the language. The loop above can also be written as follows:
for x in 1..4 {
println!("{}", x); // prints 1, then 2, then 3
}
So we know how to create an iterator from a collection, but how about the other way around? The
collect
method builds some kind of collection from an iterator:
let v2: Vec<i32> = (1..4).collect();
println!("{:?}", v2); // prints [1, 2, 3]
The type annotation on v2
is required here because the return type of
collect
is parameterized. This is one of the more advanced features of Rust’s type
system, and I haven’t even written about type parameters yet, so for now I’ll just explain how it
works with the collect
method.
Because collect
can return any type that implements FromIterator, we need to make
sure the compiler can infer the right type. In the example, we do this by giving v2
an
explicit type. Another way is to specify the type parameter for collect
directly using
the turbofish operator (yes, that’s what it’s called):
let v2 = (1..4).collect::<Vec<i32>>();
There’s one more detail here: we have to tell the compiler to use a vector because it can’t figure that out by itself, but once we do so it can figure out that the vector’s items will be integers. Naturally, Rust has a special syntax for that: in type annotations, the underscore means “you figure out what type goes here”, so either of these will also work:
let v2: Vec<_> = (1..4).collect();
let v2 = (1..4).collect::<Vec<_>>();
The count
method is less complicated. It just counts the number of items:
println!("{}", (1..4).count()); // prints 3
The all
method takes a predicate (a function that returns boolean) and tells you
whether it returns true for all of the iterator’s items. The any
method tells you if
it does for any of them:
let v = vec![0, 1, 2, 3];
let all_positive = v.iter().all(|&n| n > 0); // false
let any_positive = v.iter().any(|&n| n > 0); // true
The third group of methods are ones that take an iterator and return another one. They’re also called iterator adaptors. If you’ve used a functional programming language before you’re probably familiar with map and filter:
let v = vec![11, 22, 33];
let v2: Vec<i32> = v.iter().map(|&n| n + 1).collect();
println!("{:?}", v2); // prints [12, 23, 34]
let v2: Vec<&i32> = v.iter().filter(|&n| *n > 20).collect();
println!("{:?}", v2); // prints [22, 33]
Why did we use *n
in that last closure? The reason is that filter
passes
the items by reference, but the item type with v.iter()
is also a reference type, so it
actually passes a reference to a reference! Using &n
for the argument gets rid of one
layer of indirection, and using *n
the other.
The copied
method offers another solution: it turns an iterator over &T
into one over T
by copying each element. It only works if T
has copy
semantics, like the integer type in our example. Using copied
also means the result
is no longer a vector of references:
let v2: Vec<i32> = v.iter().copied().filter(|&n| n > 20).collect();
In this case that’s probably preferrable; a vector of references to integers is a bit of an odd
choice. There’s also a cloned
method for types that don’t have copy semantics but
implement the Clone trait.
The take
method, which takes the first n items, and the skip
method, which skips the first n, are also from functional programming (you might know
skip
as drop
):
let v2: Vec = v.iter().copied().take(2).collect();
println!("{:?}", v2); // prints [11, 22]
let v2: Vec = v.iter().copied().skip(2).collect();
println!("{:?}", v2); // prints [33]
There’s more to iterators, but this article is already pretty long! I might write another one on how to use iterators together with Option, Result, or strings. For now, I’ll refer to the documentation on the iter module and the Iterator trait for more info.