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<i32> = v.iter().copied().take(2).collect();
println!("{:?}", v2); // prints [11, 22]
let v2: Vec<i32> = 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.