RustIterators

Iterators in Rust

Iterators are one of the most powerful and elegant features in Rust. They let you process sequences of values in a composable, expressive, and highly efficient way. Understanding iterators unlocks a functional programming style that compiles down to the same machine code as a hand-written loop.

The Iterator Trait

At its core, an iterator is any type that implements the Iterator trait. The trait has one required method — next — and dozens of provided methods built on top of it.

RUST
pub trait Iterator {
    type Item;

    // The only method you must implement
    fn next(&mut self) -> Option<Self::Item>;

    // All other methods (map, filter, fold, …) are provided for free
}

next returns Some(item) while there are items left, and None when the sequence is exhausted. That simple contract is what makes the whole ecosystem composable.

Iterators Are Lazy

Creating an iterator does nothing by itself. No computation runs until you actually consume the iterator. This is called lazy evaluation and it is fundamental to how Rust achieves zero-cost abstractions.

RUST
let v = vec![1, 2, 3, 4, 5];

// This creates an iterator — but does no work yet
let iter = v.iter().map(|x| x * 2).filter(|x| x > &4);

// Work only happens here, when we consume it
for val in iter {
    println!("{}", val);
}
Note
If you create an iterator chain and never consume it, the compiler will warn you that the expression has no effect. Nothing will execute.
Creating Iterators

The standard library provides three ways to turn a collection into an iterator, and they differ in how they handle ownership.

Method

Borrows?

Item type

Collection usable after?

.iter()

Immutable borrow

&T

Yes

.iter_mut()

Mutable borrow

&mut T

Yes (no other refs)

.into_iter()

Takes ownership

T

No — moved into iterator

RUST
let nums = vec![10, 20, 30];

// iter() — borrows, yields &i32
for n in nums.iter() {
    println!("{}", n); // n is &i32
}
println!("{:?}", nums); // still usable

// iter_mut() — mutable borrow, yields &mut i32
let mut data = vec![1, 2, 3];
for n in data.iter_mut() {
    *n *= 10; // dereference to modify
}
println!("{:?}", data); // [10, 20, 30]

// into_iter() — moves the vec, yields i32
let words = vec!["hello", "world"];
for w in words.into_iter() {
    println!("{}", w); // w is &str (the owned item)
}
// println!("{:?}", words); // ERROR — words was moved
Tip
A `for` loop automatically calls `.into_iter()` on the value you give it. Writing `for x in &vec` is shorthand for `for x in vec.iter()`, and `for x in &mut vec` is shorthand for `for x in vec.iter_mut()`.
The for Loop Is Iterator Sugar

Rust's for loop is syntactic sugar over the Iterator trait. The compiler desugars it into an explicit loop that calls next() until None is returned.

RUST
let v = vec![1, 2, 3];

// What you write:
for x in v.iter() {
    println!("{}", x);
}

// What the compiler sees:
let mut iter = v.iter();
loop {
    match iter.next() {
        Some(x) => println!("{}", x),
        None => break,
    }
}
Consuming Adaptors

Methods that call next() until the iterator is empty are called consuming adaptors. After calling one of these, the iterator is gone. Below are the most commonly used ones.

collect

.collect() gathers all items into a collection. You usually need to annotate the return type so Rust knows which collection to build.

RUST
let v = vec![1, 2, 3, 4, 5];

// Collect into a Vec<i32>
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]

// Collect into a HashSet<i32>
use std::collections::HashSet;
let set: HashSet<i32> = v.iter().copied().collect();
println!("{:?}", set); // {1, 2, 3, 4, 5} (unordered)
sum and product

RUST
let nums = vec![1, 2, 3, 4, 5];

let total: i32 = nums.iter().sum();
println!("Sum: {}", total); // 15

let product: i32 = nums.iter().product();
println!("Product: {}", product); // 120
count, max, and min

RUST
let v = vec![3, 1, 4, 1, 5, 9, 2, 6];

println!("Count: {}", v.iter().count());        // 8
println!("Max:   {}", v.iter().max().unwrap()); // 9
println!("Min:   {}", v.iter().min().unwrap()); // 1
Note
`max()` and `min()` return `Option` because the iterator could be empty. Always handle the `None` case in production code instead of blindly calling `.unwrap()`.
any and all

.any() returns true if at least one element satisfies the predicate. .all() returns true only if every element satisfies the predicate. Both short-circuit: they stop consuming items as soon as the result is determined.

RUST
let scores = vec![42, 78, 95, 61, 88];

let has_perfect = scores.iter().any(|&s| s == 100);
println!("Has perfect score: {}", has_perfect); // false

let all_passing = scores.iter().all(|&s| s >= 60);
println!("All passing: {}", all_passing); // false (42 < 60)
find and position

.find() returns a reference to the first element matching a predicate. .position() returns the index (as usize) of the first match.

RUST
let words = vec!["apple", "banana", "cherry", "date"];

let found = words.iter().find(|&&w| w.starts_with('c'));
println!("{:?}", found); // Some("cherry")

let idx = words.iter().position(|&w| w == "banana");
println!("{:?}", idx); // Some(1)
fold

.fold(initial, closure) is the general reduction primitive. It starts with an accumulator set to initial, then calls the closure for each item — the closure receives the current accumulator and the item, and returns the new accumulator. .sum() and .product() are specialised folds.

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

// Compute sum with fold
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {}", sum); // 15

// Build a string from words
let words = vec!["Rust", "is", "fast"];
let sentence = words.iter().fold(String::new(), |mut acc, &w| {
    if !acc.is_empty() { acc.push(' '); }
    acc.push_str(w);
    acc
});
println!("{}", sentence); // "Rust is fast"
for_each

.for_each() calls a closure on each item purely for side effects. It fits naturally at the end of an iterator chain where a for loop would require you to break the chain into a separate variable first.

RUST
let v = vec![1, 2, 3];

v.iter().for_each(|x| println!("{}", x));
// 1
// 2
// 3
Implementing Your Own Iterator

Any struct that implements Iterator becomes a full first-class iterator — every adaptor in the standard library just works. Here is the classic Fibonacci sequence as a custom iterator.

RUST
struct Fibonacci {
    curr: u64,
    next: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { curr: 0, next: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let result = self.curr;
        let new_next = self.curr + self.next;
        self.curr = self.next;
        self.next = new_next;
        Some(result) // Infinite — never returns None
    }
}

fn main() {
    // Take the first 10 Fibonacci numbers
    let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
    println!("{:?}", fibs);

    // Sum the Fibonacci numbers below 100
    let below_100: u64 = Fibonacci::new()
        .take_while(|&n| n < 100)
        .sum();
    println!("Sum below 100: {}", below_100);
}
Tip
Because `Fibonacci` implements `Iterator`, you get `.take()`, `.map()`, `.filter()`, `.sum()`, and every other standard adaptor for free — without writing a single line of extra code.
Iterator vs For Loops

Both accomplish the same goal, but iterator chains are often more expressive for data transformation pipelines. Consider the difference between these two approaches when filtering and squaring even numbers.

RUST
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Imperative style with a for loop
let mut result = Vec::new();
for &x in &data {
    if x % 2 == 0 {
        result.push(x * x);
    }
}

// Functional style with iterators — same result, more declarative
let result: Vec<i32> = data
    .iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * x)
    .collect();

println!("{:?}", result); // [4, 16, 36, 64, 100]

The iterator version reads like a description of what you want: keep the evens, then square them. The for loop describes how to do it step by step. Both are idiomatic Rust, but iterators excel when pipelines grow longer or need to be passed around as values.

Zero-Cost Abstractions

Rust's iterators are a prime example of the zero-cost abstraction guarantee: you do not pay for what you do not use, and you cannot hand-write it more efficiently. The compiler (via LLVM) sees through the chain of closures and fuses them into a single tight loop — often with SIMD vectorisation on top.

RUST
// This iterator chain…
let sum: i64 = (0i64..1_000_000)
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .sum();

// …compiles to roughly the same assembly as this hand-written loop:
let mut sum: i64 = 0;
let mut i: i64 = 0;
while i < 1_000_000 {
    if i % 2 == 0 {
        sum += i * i;
    }
    i += 1;
}

println!("{}", sum);
Success
There is no heap allocation, no virtual dispatch, and no intermediate `Vec` created between steps. The optimizer sees the full pipeline and produces optimal machine code.
Common Patterns and Gotchas
  • Always annotate the type when using .collect() — Rust needs to know the target collection.

  • Use .copied() or .cloned() to convert &T items to T before collecting.

  • .iter() yields references — remember to dereference inside closures with &x or *x.

  • Infinite iterators like .cycle() or custom ones must be terminated with .take(n) or a similar limiter.

  • .sum() and .product() require the item type to implement Sum / Product — always true for numeric primitives.

  • Calling a consuming adaptor twice on the same iterator will not compile — the iterator is moved on first use.

Summary

Iterators in Rust are built on a single trait with one required method (next), and the entire rich standard library API — dozens of adaptors and consumers — is derived from that. They are lazy by default, so you build up a pipeline description and pay nothing until you consume it. Custom iterators are first-class: implement the trait once and every adaptor works automatically. And thanks to the zero-cost guarantee, an elegant functional chain compiles to the same tight loop a systems programmer would write by hand.