Rust: Iterators

Home · Blog

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);
}

Creating iterators

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
}

Consuming iterators

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

Transforming iterators

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]

More on iterators

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.