Iterators in Rust

Iterators in Rust provide a powerful and flexible way to process data efficiently by transforming, filtering, and aggregating elements in a collection.

Unlike traditional loops, Rust’s iterators are lazy—meaning they don't perform any actions until explicitly instructed to. This laziness makes iterators efficient because they only evaluate elements when needed, often combining multiple transformations into a single pass over the data.

In this lesson, we’ll dive into the core of Rust's iterator system and explore how to use methods like .map(), .filter(), and .fold() (similar to reduce) to create expressive, functional code.

If you prefer a video version:

All the code is available on Github (link in the video description)

You can find me here: https://francescociulla.com

Introduction to Iterators

The iterator pattern allows you to perform tasks on a sequence of items in turn. An iterator handles the logic of moving from one item to the next and determines when the sequence has finished, freeing you from implementing that logic manually. In Rust, iterators are lazy, meaning they don’t do any work until you call methods that consume the iterator.

For Example:

fn main() {
    let v1 = vec![1, 2, 3];
    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }
}

In this code, we create an iterator over a vector using the .iter() method. We then use a for loop to iterate over each value in the iterator and print it. The iterator pattern allows us to abstract away the logic of iterating through the vector, making our code more readable and expressive.

The Iterator Trait and the next Method

The Iterator trait is the core trait for iterators in Rust. It defines the behavior of the .next() method, which returns an Option containing the next value in the sequence. When the iterator is finished, .next() returns None.

All iterators in Rust implement the Iterator trait, which defines a single required method: next.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • type Item: This is an associated type that specifies the type of items the iterator will yield.

  • next(): Advances the iterator and returns the next item, or None when the iterator is exhausted.

For Example:

fn main() {
    let v1 = vec![1, 2, 3];
    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

In this code, we create an iterator over a vector and call the .next() method to get the next value in the sequence. We use the assert_eq! macro to check that the values returned by .next() are correct.

Core Types of Iterators

Rust provides three main types of iterators: consuming adaptors, iterator adaptors, and iterators themselves. Consuming adaptors take ownership of the iterator and consume it, producing a single value. Iterator adaptors take an iterator and return a new iterator with a different behavior. Iterators themselves are the base trait for all iterators and define the behavior of the .next() method.

The three main types of iterators are:

  • iter(): This method creates an iterator that borrows each element of the collection.

  • into_iter(): This method creates an iterator that takes ownership of each element of the collection.

  • iter_mut(): This method creates an iterator that mutably borrows each element of the collection.

Let's see the purpose, ownership and use case of each iterator type:

iter()

  • Purpose: This method creates an iterator that borrows each element of the collection.

  • Ownership: The original collection remains intact after calling iter().

  • Use Case: Use iter() when you want to iterate over the collection without taking ownership of it.

For Example:

let numbers = vec![1, 2, 3];

for num in numbers.iter() {
    println!("{}", num); // Outputs each number
}

println!("after iter: {:?}", numbers); // Outputs: [1, 2, 3]

iter_mut()

  • Purpose: This method creates an iterator that mutably borrows each element of the collection.

  • Ownership: The original collection remains intact after calling iter_mut().

  • Use Case: Use iter_mut() when you want to iterate over the collection and modify its elements.

For Example:

let mut numbers = vec![1, 2, 3];
for num in numbers.iter_mut() {
    *num += 1; // Mutates each element by adding 1
    println!("{}", num);
}

println!("{:?}", numbers); // Outputs: [2, 3, 4]

into_iter()

  • Purpose: This method creates an iterator that takes ownership of each element of the collection.

  • Ownership: The original collection is consumed after calling into_iter().

  • Use Case: Use into_iter() when you want to move each element out of the collection.

For Example:

let numbers = vec![1, 2, 3];

for num in numbers.into_iter() {
    println!("{}", num); // Outputs each number
}

// println!("{:?}", numbers); // Error: `numbers` is no longer accessible

Summary table of iterator types:

Here is a summary table of the three main types of iterators in Rust:

Iterator TypePurposeOwnershipUse Case
iter()Creates an iterator that borrows each element of the collection.Original collection remains intact.Iterate over the collection without taking ownership.
iter_mut()Creates an iterator that mutably borrows each element of the collection.Original collection remains intact.Iterate over the collection and modify its elements.
into_iter()Creates an iterator that takes ownership of each element of the collection.Original collection is consumed.Move each element out of the collection.

Methods to Modify or Consume Iterators: .map(), .filter(), and .fold()

Rust provides a variety of methods to modify or consume iterators, allowing you to transform, filter, and aggregate elements efficiently. These methods are called iterator adaptors and are chained together to create expressive, functional code.

Some common iterator adaptors include:

  • .map(): Transforms each element in the iterator.
  • .filter(): Filters elements based on a predicate function.
  • .fold(): Aggregates elements into a single value.

Let's explore each of these methods in more detail:

.map() - Transforming Elements

The .map() method transforms each element in the iterator by applying a closure to it. The closure takes an element as input and returns a new value, which is then yielded by the iterator.

let numbers = [1, 2, 3, 4, 5];
let squares: Vec<_> = numbers.iter().map(|&x| x * x).collect();
println!("Map - Squares: {:?}", squares); // Outputs: [1, 4, 9, 16, 25]

In this code, we use the .map() method to square each element in the numbers array. The closure |&x| x * x squares the input x, and the resulting values are collected into a new vector.

.filter() - Filtering Elements

The .filter() method filters elements in the iterator based on a predicate function. The closure takes an element as input and returns a boolean value indicating whether the element should be included in the output.

let numbers = [1, 2, 3, 4, 5];

let evens: Vec<_> = numbers.iter().filter(|&x| x % 2 == 0).collect();

println!("Filter - Evens: {:?}", evens); // Outputs: [2, 4]

In this code, we use the .filter() method to select only the even numbers from the numbers array. The closure |&x| x % 2 == 0 checks if the input x is even, and the resulting values are collected into a new vector.

.fold() - Aggregating Elements

The .fold() method aggregates elements in the iterator into a single value. It takes an initial value and a closure that combines the current accumulator value with each element in the iterator. It's similar to the reduce function in other languages.

let numbers = [1, 2, 3, 4, 5];

let sum = numbers.iter().fold(0, |acc, &x| acc + x);

println!("Fold - Sum: {}", sum); // Outputs: 15

In this code, we use the .fold() method to calculate the sum of all elements in the numbers array. The initial value 0 is passed as the first argument, and the closure |acc, &x| acc + x adds each element x to the accumulator acc.

Summary

Rust’s iterators provide a flexible way to process data efficiently by allowing transformations, filtering, and aggregations over collections. Key takeaways:

Laziness: Iterators do nothing until explicitly consumed.

Ownership and Mutability Control: Choose between iter(), iter_mut(), and into_iter() for precise control over element handling.

Functional Methods: Methods like .map(), .filter(), and .fold() allow expressive, functional transformations.

Iterators give fine-grained control over data ownership and mutability during iteration, making Rust efficient and powerful in processing collections.

If you prefer a video version:

All the code is available on Github (link in the video description)

You can find me here: https://francescociulla.com

1
Subscribe to my newsletter

Read articles from Francesco Ciulla directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Francesco Ciulla
Francesco Ciulla

👋 Hi, I Am Francesco I am a Computer Scientist interested in Web3 and DevRel. I worked from 2017 to 2020 on the Copernicus project for the ESA European Space Agency as a Fullstack Developer. Docker Captain I have interviewed 195+ Developers on my YouTube Channel I am a Developer Advocate at daily.dev I have founded 4C, a community focused on Content Creation.