The Iterator Pattern: So Simple, It's Genius (Or So They Say)
Did you know that the foreach loop is a syntactic construct that provides a more concise and readable way to iterate over a collection of…
Did you know that the foreach
loop is a syntactic construct that provides a more concise and readable way to iterate over a collection of elements? It is implemented using the iterator pattern, which is a design pattern that allows us to access the elements of a collection one at a time.

According to Refactoring.guru
Iterator is a behavioral design pattern that lets you traverse elements of a collection without exposing its underlying representation (list, stack, tree, etc.).
Based on the definition of the iterator pattern, any object that implements the iterator interface should be able to be iterated over using the foreach loop. This is because the foreach loop does not depend on the underlying type of the object being iterated over. It simply iterates over the elements of the object one at a time, using the Iterator trait to access the next element.
Iterator pattern implementation in Rust
The Iterator type
The Iterator
trait in Rust is a generic trait that defines the methods necessary to iterate over a collection of elements. It is defined as follows:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
The type Item
defines the type of the elements that are being iterated over. The next()
method returns the next element in the collection, or None
if there are no more elements.
The Iterator trait has many methods, but the most important one is the next()
method. This method is required to be implemented by any type that wants to be an iterator. The next()
method returns the next element in the collection, or None
if there are no more elements

For loop in Rust
Let’s take a look at this piece of code.

The first block of code will compile because the variable a
is a vector, which implements the Iterator trait. The second block of code will throw an error because the variable b
is an integer, which does not implement the Iterator trait. Therefore, we cannot use a for loop with the variable b
.
It is important to note that the Vec
type does not implement the Iterator trait, but the for loop is still available on the type. This is because the for loop is implemented in terms of the iter() method, which is provided by the Vec
type. The iter()
method returns an iterator over the elements of the vector, which can then be used in a for loop.
This is one of the rare implicit behaviors in Rust, which is designed to make the code cleaner and more concise. The compiler can implicitly call the into_iter()
method of the IntoIterator
trait when the type does not implement the Iterator
trait. This allows us to use the for loop with types that do not directly implement the Iterator
trait.
The difference between vec.iter()
and vec.into_iter()
is that the former consumes the vector and returns an iterator that owns the elements of the vector. The latter does not consume the vector and returns an iterator that borrows the elements of the vector. This means that the former can only be used once, while the latter can be used multiple times.
The next method
We will explain the next()
method by implementing the Iterator
trait for our type. First, we define a type called PrimeNumberSequence
that produces the sequence of prime numbers.
pub struct PrimeNumberSequence {
current_value: u32,
}
impl PrimeNumberSequence {
pub fn new() -> Self {
Self { current_value: 0 }
}
//this method check if a particular number is a prime_number
fn is_prime(&self, n: u32) -> bool {
if n <= 1 {
return false;
}
for a in 2..n {
if n % a == 0 {
return false; // if it is not the last statement you need to use `return`
}
}
true // last value to return
}
}
Next, we will implement the methods required by the Iterator
trait for the PrimeNumberSequence
type.
impl Iterator for PrimeNumberSequence {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.is_prime(self.current_value) {
let result = self.current_value;
//if the current_value is prime, we increase it
//so the next time we call next, it would not emit the same value
self.current_value += 1;
return Some(result);
}
//if the current_value is not prime, we simply increase it
self.current_value += 1;
}
}
}
Next, we will modify the main()
function to create an instance of the PrimeNumberSequence
type and iterate over the elements of the sequence.
fn main() {
let prime_sequence = PrimeNumberSequence::new();
for prime_number in prime_sequence.take(3){
println!("{}", prime_number);
}
}
The code compiles just fine and prints out 2,3,5.

The next()
method of the PrimeNumberSequence
type returns the next prime number in the sequence. This allows us to iterate over the sequence one prime number at a time, without having to pre-calculate the entire sequence. This is a memory-efficient approach, as we only need to store the current prime number in memory.
The collect()
method returns an iterator over the entire sequence. This can be useful if we want to collect all of the elements into a list or array. However, calling the collect()
method will force the sequence to evaluate the entire sequence, which can be expensive if the sequence is large.
Delegating the iterator
I have also seen other developers use a technique called delegation to hide the underlying data type of a collection. Delegation is a design pattern that allows one object to defer responsibility to another object. In this case, the iterator object delegates the responsibility of iterating over the elements of the collection to the parent type. This allows the iterator object to remain agnostic about the underlying data type of the collection.
pub struct Class {
pub students: Vec<Student>,
}
pub struct Student {
pub name: String,
}
impl Class {
//this iter() hides the students vec and reveals only the Iter<'_, Student>
pub fn iter(&self) -> Iter<'_, Student> {
self.students.iter()
}
}
In this case, we do not want to expose the students
property directly, but rather only expose it through the iterator returned by the self.students.iter()
method. This allows us to freely change the underlying type of the students
without affecting the behavior of the consumer.
fn main() {
let class = Class {
students: vec![
Student {
name: "A".to_string(),
},
Student {
name: "B".to_string(),
},
Student {
name: "C".to_string(),
},
],
};
for student in class.iter(){
println!("{}", student.name);
}
}
Functional method on Arrays
Array.Map in C# and JS
In C#, JavaScript, and TypeScript, the Array.Map method is a built-in function that allows you to map a sequence of elements to a new sequence of elements. The function that is used to map the elements can be any function that takes an element of the original sequence and returns an element of the new sequence.
In Rust, there is no built-in Array.Map function. However, the same functionality can be achieved using the map()
function from the std::iter
module. The map()
function takes an iterator and a function as arguments, and returns a new iterator that applies the function to each element of the original iterator.
This is how Map() is defined in C#

In essence, if we call the Select()
method on an IEnumerable<T>
, the method will return an IEnumerable<TResult>
, where TResult
is the type of the output sequence and T
is the inner type of the input sequence.
The equivalent of the Select()
method in JavaScript is the map()
method. The map()
method takes an iterable object and a function as arguments, and returns a new iterable object that applies the function to each element of the original iterable object.

In other words, the Map
function can be represented by the following pseudocode:
[T] -> Map(Fn<T, R>) -> [R]
Here is a breakdown of the pseudocode:
- The first
[T]
represents an iterable object of typeT
. - The
Map()
function takes a function as its argument. The function takes an element of typeT
as its input and returns an element of typeR
as its output. - The
->
symbol represents the function application operator. - The second
[R]
represents an iterable object of typeR
.
Map in Rust
map
method in Rust returns Map type instead of an Iterator directly.
Map<I,F>
(An iterator that maps the values of iter
with f
.) instead of an Iterator
.

Other iterator methods in Rust, such as filter
, flat_map
, and flatten
, also return a wrapper type instead of an Iterator.


The Map
and Filter
type are both iterators, but they are also more than that. They are wrappers around iterators that provide additional functionality, such as the ability to map or filter the elements of the iterator.

The Map
class is a similar example of delegating the iterator from the underlying type. The next()
method of the Map
class does not do anything special; it simply calls the next()
method of the inner iterator and maps the result to the desired type.
Iterators do not evaluate their elements until they are needed. This is called lazy evaluation. We can consume an iterator by calling the next()
method, which will evaluate the next element in the iterator
Take it one step further
Option and Result
The Option
and Result
types are both special types that can be used to represent the absence or presence of a value, or the success or failure of an operation. They both implement the Iterator
trait, which means that they can be used in a pipeline of iterators. The map()
function is a method of the Iterator
trait that can be used to apply a function to each element of an iterator. In the context of Option
and Result
, the map()
function can be used to apply a function to the value of the Option
or Result
, if it is present.
pub fn map<U, F>(self, f: F) -> Option<U> where
F: FnOnce(T) -> U,
pub fn map<U, F>(self, op: F) -> Result<U, E>where
F: FnOnce(T) -> U,
Pagination
We can create a function take()
that takes a Skip
iterator as its argument. This ensures that the take()
function can only be used on a sequence that has been skipped. This is useful for preventing errors that can occur when taking elements from a sequence that has not been skipped.
let vec = vec![1, 2, 3, 4, 5];
let skipped = take(vec.iter().skip(3), 3);
for number in skipped {
println!("{}", number);
}
Conclusion
I hope this brief post has given you a general understanding of the Iterator pattern and how for loops work in Rust.
I hope you found this post helpful. If so, please clap and follow me for more posts in the future. If you would like to support my work, you can buy me a coffee.