Ranges and lazy sequences
A range is a pair of bounds that implements Iterator when the element type is a step. 0..5 stops before five and 0..=5 includes it, and both are lazy: nothing is generated until the range is consumed. That is why a range of a million elements costs nothing until it is walked.
The lesson covers the two forms, the direction and the step, the character range, the laziness and the one restriction that surprises people: floats do not implement the stepping trait, so a range of floats is not an iterator. The whole sequence fits in one syntax and one trait.
Range and RangeInclusive
The exclusive form 0..5 yields zero to four, and the inclusive form 0..=5 yields zero to five. The end value is the only difference, and the type of the value decides which is natural for a loop over indices.
The program sums an exclusive range and collects an inclusive one. The sum is ten and the collected vector holds one, two and three.
fn main() {
let sum: i32 = (0..5).sum();
let inclusive: Vec<i32> = (1..=3).collect();
let reversed: Vec<i32> = (1..=5).rev().collect();
let stepped: Vec<i32> = (1..=10).step_by(3).collect();
println!("{sum} {inclusive:?} {reversed:?} {stepped:?}");
}
rustc --edition 2024 range.rs
./range
10 [1, 2, 3] [5, 4, 3, 2, 1] [1, 4, 7, 10]
The exclusive form is the one a slice loop uses, because a length is one past the last index. The two dots are also the syntax for a slice, which the lesson on slices uses.
rev and step_by
rev walks an iterator backwards and needs a double-ended iterator, which a range provides. step_by takes every n-th item, and the two compose in either order.
The program counts down from five to one and takes every third value of one to ten. The outputs are the reversed list and the four stepped values.
fn main() {
let reversed: Vec<i32> = (1..=5).rev().collect();
let stepped: Vec<i32> = (1..=10).step_by(3).collect();
println!("{reversed:?}");
println!("{stepped:?}");
}
rustc --edition 2024 rev_step.rs
./rev_step
[5, 4, 3, 2, 1]
[1, 4, 7, 10]
Both adaptors are lazy and cost nothing until the consumer runs. A loop that steps with an index and a condition is the hand-written version of the same two calls.
A range of characters
Characters are ordered by their code point, so 'a'..='e' is a valid range and yields the five letters in order. It is the shortest way to generate an alphabet slice.
The program collects the five letters and prints them. The debug form shows the characters with their quotes.
fn main() {
let letters: Vec<char> = ('a'..='e').collect();
println!("{letters:?}");
}
rustc --edition 2024 chars.rs
./chars
['a', 'b', 'c', 'd', 'e']
The range covers the code points between the two letters, which for ASCII is the alphabet. The lesson on characters explains what a code point is.
Ranges are lazy
A range of a million values is two numbers, not a million. The iterator produces each value on demand, and a consumer such as take stops the generation after the requested count.
The program sums the first three values of a range of one million and prints three. Nothing else was generated, which is why the program returns immediately.
fn main() {
let numbers = 0..1_000_000;
let first: i32 = numbers.take(3).sum();
println!("{first}");
}
rustc --edition 2024 lazy.rs
./lazy
3
The same property makes contains on a range a constant-time check: the range is two bounds, and the test compares them. The next section shows the call.
A range of floats
The stepping trait is implemented for integers and characters, not for floats, so 0.0..1.0 is not an iterator. A loop over it is E0277, and the note points at the nightly-only trait behind the restriction.
The program loops over a float range. The fix is an integer range scaled by a step, or an iterator of computed values, because a float has no exact successor.
fn main() {
for step in 0.0..1.0 {
println!("{step}");
}
}
rustc --edition 2024 float_range_err.rs
error[E0277]: `std::ops::Range<{float}>` is not an iterator
--> float_range_err.rs:2:17
|
2 | for step in 0.0..1.0 {
| ^^^^^^^^ `Range<{float}>` is not an iterator
|
= help: the nightly-only, unstable trait `Step` is not implemented for `{float}`
= note: `Range` only implements `Iterator` for select types in the standard library, particularly integers; to see the full list of types, see the documentation for the unstable `Step` trait
= help: the following other types implement trait `Step`:
i128
i16
i32
i64
i8
isize
u128
u16
and 4 others
= note: required for `std::ops::Range<{float}>` to implement `Iterator`
= note: required for `std::ops::Range<{float}>` to implement `IntoIterator`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
A scaled integer loop is the usual replacement: multiply the index by the step and divide at the end, which keeps the arithmetic exact. The restriction is the same reason a float cannot be a set key: there is no total, exact order. The lesson on numbers explains the representation.
Exercises
- Print the even numbers from zero to ten with a range and a filter.
- Generate the letters a to f in reverse and collect them into a vector.
- Sum every fifth number from one to one hundred and check the answer by hand.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which range includes its end value?
Answer
0..=5
The equals sign after the two dots makes the end inclusive.
-
What does
revrequire of the iterator?Answer
That it is double-ended
Walking backwards needs an iterator that can take items from the other end.
-
What does
step_by(3)yield from 1..=10?Answer
1, 4, 7, 10
The first item is kept and every third after it.
-
Why is a float range not an iterator?
Answer
Floats do not implement the stepping trait
The range iterator needs a successor, and a float has no exact one.
-
What does this program print?
fn main() { let sum: i32 = (0..5).sum(); let values: Vec<i32> = (1..=3).collect(); println!("{sum} {values:?}"); }Answer
10 [1, 2, 3]
The exclusive range sums zero to four, and the inclusive one collects one to three.
-
What does this program print?
fn main() { let reversed: Vec<i32> = (1..=5).rev().collect(); let stepped: Vec<i32> = (1..=10).step_by(3).collect(); println!("{reversed:?}"); println!("{stepped:?}"); }Answer
[5, 4, 3, 2, 1] / [1, 4, 7, 10]
The reverse walks down from five, and the step keeps one and every third after it.
-
What does this program print?
fn main() { let letters: Vec<char> = ('a'..='e').collect(); println!("{letters:?}"); }Answer
['a', 'b', 'c', 'd', 'e']
The range walks the code points of the five letters in order.
-
What does this program print?
fn main() { let numbers = 0..1_000_000; let first: i32 = numbers.take(3).sum(); println!("{first}"); }Answer
3
The range is lazy, and take stops it after three values.
-
What is the first line of the compiler output for this program?
fn main() { for step in 0.0..1.0 { println!("{step}"); } }Answer
error[E0277]:
std::ops::Range<{float}>is not an iteratorThe range needs a stepping type, and a float does not implement it.
-
What does this program print?
fn main() { let range = 1..10; println!("{} {}", range.contains(&5), range.contains(&10)); }Answer
true false
The range excludes ten, so only five is inside.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev