Rust Iterators: enumerate(), rev(), laziness, and positional indices
Rust iterators are composable, which is powerful, but it also means that method order matters.
This is especially important when combining enumerate() and rev().
If you use indices as positional values, such as powers, offsets, ranks, or positions from the right side of a sequence, changing the method order can silently change the meaning of your code.
let v = ["a", "b", "c", "d", "e", "f"];The examples below use this same array and show four different patterns.
Why Order Matters
enumerate() does one thing:
it adds an index to each item in the current iteration order.
rev() does another thing:
it reverses the current iterator.
So these two are not interchangeable:
v.iter().enumerate().rev()and:
v.iter().rev().enumerate()They contain the same values, but not the same index meaning.
Iterators Are Lazy
Rust iterators behave a lot like Python generators: building the iterator chain does not process all values immediately.
let iter = v.iter().rev().enumerate();At this point, Rust has not printed anything, collected anything, or walked through the array. It has only built a lazy iterator pipeline.
The work happens when the iterator is consumed.
Python has a similar idea with generators:
items = ["a", "b", "c"]
gen = enumerate(reversed(items))This does not produce all pairs immediately. The values are produced as the generator is consumed.
for ix, item in gen:
print(ix, item)Rust works with the same general mental model: iterator adapters describe a pipeline, and consumers run that pipeline.
When the Work Actually Happens
A for loop consumes an iterator:
for (ix, e) in v.iter().rev().enumerate() {
println!("{ix} -> {e}");
}So does collect():
let pairs: Vec<_> = v.iter().rev().enumerate().collect();And so do methods like count(), sum(), fold(), and max_by_key():
let count = v.iter().count();
let total_length: usize = v.iter().map(|e| e.len()).sum();
let longest = v.iter().max_by_key(|e| e.len());These are different from iterator adapters like iter(), rev(), enumerate(), map(), and filter(). Adapters usually build another lazy iterator. Consumers actually pull values through the pipeline.
Big-O Implications
Creating an iterator chain is usually cheap.
let iter = v.iter().rev().enumerate();This is generally O(1) work: Rust creates a small iterator value that knows how to walk the data later.
Consuming it is where the O(n) work happens:
for (ix, e) in v.iter().rev().enumerate() {
println!("{ix} -> {e}");
}This visits every element once, so it is O(n) time.
If you collect the result:
let pairs: Vec<_> = v.iter().rev().enumerate().collect();Then it is:
O(n)time to visit all elementsO(n)memory to store the newVec
But this:
v.iter().rev()does not create a reversed copy of v. It only changes the direction of traversal.
The same distinction exists in Python:
gen = enumerate(reversed(items))This is lazy-ish: it prepares a generator-style pipeline.
But this:
pairs = list(enumerate(reversed(items)))forces the whole result into memory.
That means:
- building the iterator/generator pipeline is cheap
- consuming it is where the real work happens
- collecting into a list or
Vecadds memory cost
Scenario 1: Regular Loop
This is the baseline.
println!("Scenario 1: regular");
for (ix, e) in v.iter().enumerate() {
println!(" {ix} -> {e}");
}Output:
0 -> a
1 -> b
2 -> c
3 -> d
4 -> e
5 -> f
enumerate() adds indices in normal reading order.
Use this when you want normal items with normal left-to-right indices.
Scenario 2: Reverse the Indexed Pairs
println!("Scenario 2: iter().enumerate().rev()");
for (ix, e) in v.iter().enumerate().rev() {
println!(" {ix} -> {e}");
}Output:
5 -> f
4 -> e
3 -> d
2 -> c
1 -> b
0 -> a
Here, Rust first creates indexed pairs:
0 -> a
1 -> b
2 -> c
3 -> d
4 -> e
5 -> f
Then it reverses those pairs.
So the items are reversed, but the indices still mean original positions.
Use this when you want to walk backward while keeping original positions.
Do not use this if you expected the reversed sequence to start at index 0.
Scenario 3: Reverse Values, Then Re-Index from Zero
println!("Scenario 3: iter().rev().enumerate()");
for (ix, e) in v.iter().rev().enumerate() {
println!(" {ix} -> {e}");
}Output:
0 -> f
1 -> e
2 -> d
3 -> c
4 -> b
5 -> a
Here, Rust reverses the values first:
f, e, d, c, b, a
Then enumerate() adds fresh indices:
0 -> f
1 -> e
2 -> d
3 -> c
4 -> b
5 -> a
Use this when you want reversed values with a new 0..n index.
Scenario 4: Normal Items, Reversed Indices
Sometimes you want to keep the items in normal order, but count positions from the right.
For example:
5 -> a
4 -> b
3 -> c
2 -> d
1 -> e
0 -> f
This is useful when the leftmost item should have the highest positional value and the rightmost item should have 0.
Use zip() for that:
println!("Scenario 4: (0..v.len()).rev().zip(v.iter())");
for (ix, e) in (0..v.len()).rev().zip(v.iter()) {
println!(" {ix} -> {e}");
}Output:
5 -> a
4 -> b
3 -> c
2 -> d
1 -> e
0 -> f
This is the cleanest pattern when you want:
- normal item order
- reversed positional indices
- right-to-left position values
You do not need to store v.len() in a separate variable here. v.len() is evaluated when the range is created.
Positional Math Example
The fourth pattern is useful for positional math.
Imagine each letter is a digit-like value, and the index represents its position from the right:
a b c d e f
5 4 3 2 1 0
That is the shape you want for logic like base conversion, ranking from the right, or right-aligned offsets.
for (power, digit) in (0..v.len()).rev().zip(v.iter()) {
println!("{digit} has positional power {power}");
}Output:
a has positional power 5
b has positional power 4
c has positional power 3
d has positional power 2
e has positional power 1
f has positional power 0
The point is not that every loop needs zip(). The point is that the index should match your meaning.
If the index means “position from the left,” use normal enumerate().
If the index means “position from the right,” use a reversed range and zip().
Visual Summary
| Pattern | Item Order | Index Meaning | Output Shape |
|---|---|---|---|
v.iter().enumerate() |
normal | normal | 0->a ... 5->f |
v.iter().enumerate().rev() |
reversed | original | 5->f ... 0->a |
v.iter().rev().enumerate() |
reversed | fresh from zero | 0->f ... 5->a |
(0..v.len()).rev().zip(v.iter()) |
normal | from right | 5->a ... 0->f |
TL;DR: Which One Should I Use?
Use v.iter().enumerate() when you want normal order and normal indices.
Use v.iter().enumerate().rev() when you want to go backward but keep each element’s original position.
Use v.iter().rev().enumerate() when you want reversed values and fresh indices starting from 0.
Use (0..v.len()).rev().zip(v.iter()) when you want normal values but indices counted from the right.
For Big-O intuition:
- iterator adapters like
iter(),rev(), andenumerate()are lazy - creating the chain is usually
O(1) - consuming the chain is usually
O(n) - collecting into a
VecisO(n)time andO(n)memory
Conclusion
The important rule is:
enumerate()indexes whatever order exists at that point in the chain.
So:
v.iter().enumerate().rev()means:
add indices, then reverse the indexed pairs.
But:
v.iter().rev().enumerate()means:
reverse the values, then add new indices.
Rust iterators are lazy, like Python generator pipelines, so method chains usually define work rather than immediately doing it. The actual O(n) cost appears when the iterator is consumed by a for loop, collect(), sum(), fold(), or a similar consumer.
If your index has semantic meaning, especially for positional math, be careful with method order.
For right-based positional logic, (0..v.len()).rev().zip(v.iter()) is usually the clearest pattern.