GitHub - Rustomax - Rust-Iterator
GitHub - Rustomax - Rust-Iterator
View license
Star Notifications
README License
rust-iterators
Demonstrates basic Rust iterator use.
Build Status
https://github.com/rustomax/rust-iterators/#introduction 1/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
The goal of this tutorial is to provide a handy reference to some of the common iterator
patterns. It is not meant to be a replacement for the Iterator API reference or an
overview of the core iterator concepts described in The Book. In fact, this tutorial relies
on both resources.
To take full advantage of the material described here, it is recommended that you
have at least cursory familiarity with Rust.
Contents
Introduction
Basic Ranges
Digging Deeper
Iterating over Arrays
Combining Iterator Adaptors
Ranges of Characters
Iterating over Vectors
Infinity and Beyond
Itertools
Creating Your Own Iterators
Introduction
https://github.com/rustomax/rust-iterators/#introduction 2/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
While this venerable method is powerful and flexible enough to accommodate many
scenarios, it is also responsible for a fair share of bugs ranging from misplaced
semicolons to unintentionally mutating the iterator variable inside the loop. In the spirit
of safety and consistency with other language features, the C-style for loop is absent
from Rust. Instead, Rust leverages iterators to achieve similar goals (and a lot more).
Basic Ranges
The most basic way to loop over a series of integers in Rust is the range. Range is
created using .. notation and produces an iterator of integers incremented by 1 :
for i in 1..11 {
print!("{} ", i);
}
// output: 1 2 3 4 5 6 7 8 9 10
The code above will print a series of numbers from 1 to 10 , and not include the last
number 11 . In other words, the .. produces an iterator that is inclusive on the left and
exclusive on the right. In order to get a range that is inclusive on both ends, you use the
..= notation:
for i in 1..=10 {
print!("{} ", i);
}
// output: 1 2 3 4 5 6 7 8 9 10
If you do not use the loop iterator variable, you can avoid instantiating it by leveraging
the _ pattern. For instance, the following code prints out the number of elements in the
iterator without instantiating a loop iterator variable:
The example above is somewhat contrived since iterators in Rust have count()
function, which returns the number of elements in the iterator without the need to
count them in a loop:
https://github.com/rustomax/rust-iterators/#introduction 3/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
You will find that experienced Rust programmers are able to express in very terse
iterator language what otherwise would have taken many more lines of
conventional looping code. We cover some of these patterns below as we talk about
adaptors, consumers and chaining iterator methods into complex statements.
Digging Deeper
If the basic incremental sequential range does not satisfy your needs, there are plenty of
ways in Rust to customize the range iterators. Let's look at a few common ones.
for i in (0..11).step_by(2) {
print!("{} ", i);
}
//output: 0 2 4 6 8 10
Alternatively, same result can be achieved with the filter() method. It applies a
closure that can return either true or false to each element of an iterator and
produces a new iterator that only contains elements for which the closure returns true .
The following iterator will produce a sequence of even numbers between 0 and 20.
Because filter() uses closures, it is very flexible and can be used to produce iterators
that evaluate complex conditions. For instance, the following iterator produces a series
of integers in the range between 0 and 20 that divide by both 2 and 3 without a
remainder:
https://github.com/rustomax/rust-iterators/#introduction 4/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
}
// output: 0 6 12 18
While by default ranges are incremental, they can easily be reversed using the rev()
method.
for i in (0..11).rev() {
print!("{} ", i);
}
// output: 10 9 8 7 6 5 4 3 2 1 0
Another common iterator adaptor, map() , applies a closure to each element, and
returns the resulting iterator. Here is an example of an iterator that produces a sequence
of squares of numbers from 1 to 10 :
for i in (1..11).map(|x| x * x) {
print!("{} ", i);
}
// output: 1 4 9 16 25 36 49 64 81 100
// output: result = 55
Perhaps the easiest way to understand what is happening here is to rewrite the example
above in a more procedural fashion:
for x in 1..=5 {
acc += x * x;
}
// output: result = 55
https://github.com/rustomax/rust-iterators/#introduction 5/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
Wow! Isn't the fold() version so much more concise and readable?
Similarly to iterating over ranges, we can iterate over an array. The benefit of this is that
arrays can contain values of arbitrary types, not just integrals. The only caveat is that
array is not an iterator. We need to turn it into an iterator using the iter() method.
While in the previous sections we covered a good variety of methods allowing you to
generate many different types of iterators, the real power of Rust shines when you start
combining these approaches.
let c = (1..4).chain(6..9);
for i in c {
print!("{} ", i);
}
// output: 1 2 3 6 7 8
You can get very creative combining things! Below is an iterator that combines two
ranges: the first one is incremented and filtered, another one - decremented. Not sure
what such an abomination could be used for, but here it is nonetheless!
https://github.com/rustomax/rust-iterators/#introduction 6/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
let r = (1..20)
.filter(|&x| x % 5 == 0)
.chain((6..9).rev());
for i in r {
print!("{} ", i);
}
// output: 5 10 15 8 7 6
Notice how in the example above Rust allows us to visually better represent
complex iterator statements by splitting them into multiple lines.
Ranges of Characters
Programs that manipulate strings or text often require the ability to iterate over a range
of characters. The char_iter crate provides convenient way to generate such ranges.
char_iter supports Unicode characters.
https://github.com/rustomax/rust-iterators/#introduction 7/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
[dependencies]
char-iter = "0.1"
Vector is one of Rust's fundamental structures. By its nature it is well suited to represent
series of repetitive items. There are a number of language facilities in Rust that allow
using vectors as iterators and vice-versa.
In the simplest case, similarly to how we created an iterator from an array, we can create
an iterator from a vector using the iter() method. In fact this is considered to be the
most idiomatic way in Rust to iterate over a vector.
for i in nums.iter() {
print!("{} ", i);
}
// output: 1 2 3 4 5
As a matter of fact, the pattern above is so common that rust provides syntactic sugar
for it in the form of the reference operator & .
Notice that the borrows above are immutable. In other words, they are read-only. If we
want to make changes to our vector, we have to use the mutable borrow &mut . For
instance, the following code will mutably iterate over a vector doubling each element in
the process.
https://github.com/rustomax/rust-iterators/#introduction 8/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
However, now that you are an iterator ninja, you wouldn't use the for loop syntax
above. You'd go with a map() instead, right?
This won't compile with the error message cannot borrow nums as mutable more
than once at a time. You see, our iterator (instantiated in the for loop) already
borrowed nums as mutable. The push expression tries to do that again, which is
prohibited in rust. This is rust's famous safety at work. If we could push something
into the vector, while iterating over it, this would invalidate the iterator causing
undefined behavior. Rust prevents this from happening at compile time. Not only
iterators are powerful, but they are also super safe.
Now, let's do the opposite - create a vector from an iterator. In order to do that we need
what is called a consumer. Consumers force lazy iterators to actually produce values.
collect() is a common consumer. It takes values from an iterator and converts them
into a collection of required type. Below we are taking a range of numbers from 1 to
10 and transforming it into a vector of i32 :
let v = (1..11).collect::<Vec<i32>>();
println!("{:?}", v);
https://github.com/rustomax/rust-iterators/#introduction 9/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
// output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
To get both the element of a vector and its index, you can use enumerate() method,
which returns a tuple containing the index and the item on each iteration:
There are a few other functions, that make using iterators on vectors particularly helpful.
min() and max() , for instance, return options, containing minimum and maximum
values of the vector elements respectively:
sum() returns a sum of all values in an iterator. The following program leverages the
sum() method to compute the grade point average of a rather mediocre student:
So far we have dealt with iterators that operated on some finite range of values. Rust
generalizes iterators in such a way that it is in fact possible to create an infinite range!
Let us consider the following example:
https://github.com/rustomax/rust-iterators/#introduction 10/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
let r = (1..).collect::<Vec<i32>>();
The (1..) defines a range that starts with 1 and increments indefinitely. In practice,
such program compiles and runs, but eventually crashes with the error message: fatal
runtime error: out of memory . Well, that's not very practical, you might say. Indeed, by
themselves infinite ranges are pretty useless. What makes them useful is combining
them with other adaptors and consumers.
One particularly helpful pattern involves using the take() method to limit the number
of items returned by the iterator. The following iterator will return the first 10 items in a
sequence of squares of integers that are divisible by 5 without a remainder.
let v = (1..)
.map(|x| x * x)
.filter(|x| x % 5 == 0 )
.take(10)
.collect::<Vec<i32>>();
// output: [25, 100, 225, 400, 625, 900, 1225, 1600, 2025, 2500]
Itertools
The itertools crate contains powerful additional iterator adaptors. Below are some
examples.
[dependencies]
itertools = "0.10.0"
The unique() adaptor eliminates duplicates from an iterator. The duplicates do not
need to be sequential.
for d in unique {
https://github.com/rustomax/rust-iterators/#introduction 11/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
print!("{} ", d);
}
//output: 1 4 3 2 5
The join() adaptor combines iterator elements into a single string with a separator in
between the elements.
The sorted_by() adaptor applies custom sorting order to iterator elements, returning a
sorted vector. The following program will print out top 5 happiest countries, according to
2018 World Happiness Report.
// output:
// # 1: Finland
// # 2: Norway
// # 3: Denmark
https://github.com/rustomax/rust-iterators/#introduction 12/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
// # 4: Iceland
// # 5: Switzerland
Beautiful thing about Rust is that you can use generic language facilities to extend it. Let
us leverage this awesome power and create our own iterator! We will build a very simple
iterator that produces a series of pairs of temperatures (Fahrenheit, Celsius) ,
represented by a tuple of floating-point numbers (f32, f32) . The temperature is
calculated using commonly known formula: °C = (°F - 32) / 1.8 .
An iterator starts with a struct . Whatever we name the struct will also be the name
of the iterator. We will call ours FahrToCelc . The struct contains fields that hold useful
information that persists between subsequent iterator calls. We will have two f32 fields
- the temperature in Fahrenheit, and the increment step:
struct FahrToCelc {
fahr: f32,
step: f32,
}
Next, we will create a convenience method new() that initializes the iterator by passing
it initial values for temperature in Fahrenheit and the increment step. This method is
strictly speaking not necessary and is not part of the iterator implementation, but I find
it to be a nice syntactic sugar that improves overall program readability:
impl FahrToCelc {
fn new(fahr: f32, step: f32) -> FahrToCelc {
FahrToCelc { fahr: fahr, step: step }
}
}
Finally, we program the behavior of the iterator by implementing the Iterator trait for
our struct . The trait at a minimum needs to contain the following:
Definition of the Item type. It describes what kind of things the iterator will
produce. As mentioned before our iterator produces temperature pairs
(Fahrenheit, Celsius) represented by a tuple of floating-point numbers (f32,
f32) , so our Item type definition will look like this:
https://github.com/rustomax/rust-iterators/#introduction 13/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
Function next() that actually generates the next Item . next() takes a mutable
reference to self and returns an Option encapsulating the next value. The reason
why we have to return an Option and not the item itself is because many iterators
need to account for the situation where they have reached the end of the sequence,
in which case they return None . Since our iterator generates an infinite sequence,
our next() method will always return Option<Self::Item> . Thus, our next()
function declaration looks like this:
The next() function typically also does some internal housekeeping. Ours increments
Fahrenheit temperature fahr by step so that it can be returned on subsequent
iteration. Making these modifications to internal fields is the reason why we need to
pass a mutable reference to self as a parameter to next() .
struct FahrToCelc {
fahr: f32,
step: f32,
}
impl FahrToCelc {
fn new(fahr: f32, step: f32) -> FahrToCelc {
FahrToCelc { fahr: fahr, step: step }
}
}
https://github.com/rustomax/rust-iterators/#introduction 14/15
11/22/24, 7:10 PM GitHub - rustomax/rust-iterators: Basic Rust iterator usage
fn next (&mut self) -> Option<Self::Item> {
let curr_fahr = self.fahr;
let curr_celc = (self.fahr - 32.0) / 1.8;
self.fahr = self.fahr + self.step;
Some((curr_fahr, curr_celc))
}
}
fn main() {
// pass the starting temperature and step to the initializer function
let ftc = FahrToCelc::new(0.0, 5.0);
Releases
No releases published
Packages
No packages published
Contributors 6
Languages
Rust 100.0%
https://github.com/rustomax/rust-iterators/#introduction 15/15