Processing a Series of Items with Iterators
The iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don't have to reimplement that logic yourself.
In Oxide, iterators are lazy, meaning they have no effect until you call methods that consume the iterator. This lets you chain multiple operations together efficiently.
The Iterator Trait
All iterators implement the Iterator trait from the standard library. The
trait definition looks like this:
trait Iterator {
type Item
mutating fn next(): Self.Item?
}
The trait requires you to define one method: next. Each call to next returns
one item of the iterator wrapped in Some, and when iteration is over, it
returns null.
Creating Iterators
The most common way to create an iterator is by calling a method on a collection:
fn main() {
let numbers = vec![1, 2, 3]
// Creates an iterator over references
var iter = numbers.iter()
// Manually calling next()
println!("\(iter.next():?)") // Some(1)
println!("\(iter.next():?)") // Some(2)
println!("\(iter.next():?)") // Some(3)
println!("\(iter.next():?)") // null
}
There are three common methods to create iterators from collections:
| Method | Produces | Use when |
|---|---|---|
iter() | &T (references) | You want to read values |
iterMut() | &mut T (mutable references) | You want to modify values |
intoIter() | T (owned values) | You want to take ownership |
fn main() {
var numbers = vec![1, 2, 3]
// iter() - borrows immutably
for n in numbers.iter() {
println!("Read: \(n)")
}
// iterMut() - borrows mutably
for n in numbers.iterMut() {
*n *= 2 // Double each value
}
println!("After doubling: \(numbers:?)")
// intoIter() - takes ownership
for n in numbers.intoIter() {
println!("Owned: \(n)")
}
// numbers is no longer usable here
}
Iterators Are Lazy
A critical characteristic of iterators is that they're lazy - they don't do anything until you consume them:
fn main() {
let numbers = vec![1, 2, 3]
// This does NOTHING by itself
let iter = numbers.iter().map { it * 2 }
// The work happens when we consume the iterator
let doubled: Vec<Int> = iter.collect()
}
The compiler will warn you if you create an iterator and don't use it:
warning: unused `Map` that must be used
--> src/main.ox:4:5
|
4 | numbers.iter().map { it * 2 }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
Consuming Adaptors
Methods that call next are called consuming adaptors because they use up
the iterator. Let's look at the most common ones.
sum
The sum method consumes the iterator and adds all elements:
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
let total: Int = numbers.iter().sum()
println!("Sum: \(total)") // Prints: Sum: 15
}
collect
The collect method consumes an iterator and collects the results into a
collection:
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
let doubled: Vec<Int> = numbers
.iter()
.map { it * 2 }
.collect()
println!("Doubled: \(doubled:?)") // Prints: Doubled: [2, 4, 6, 8, 10]
}
You often need to specify the target type, either with a type annotation or using the turbofish syntax:
fn main() {
let numbers = vec![1, 2, 3]
// Type annotation on the variable
let doubled: Vec<Int> = numbers.iter().map { it * 2 }.collect()
// Or turbofish syntax
let doubled = numbers.iter().map { it * 2 }.collect<Vec<Int>>()
// Or turbofish syntax
let doubled = numbers.iter().map { it * 2 }.collect<Vec<Int>>()
}
Other Consuming Adaptors
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
// count - counts elements
let count = numbers.iter().count()
// last - gets the last element
let last = numbers.iter().last()
// nth - gets the nth element (0-indexed)
let third = numbers.iter().nth(2)
// fold - reduces with an initial value and accumulator
let product: Int = numbers.iter().fold(1, { acc, x -> acc * x })
// any - checks if any element satisfies a predicate
let hasEven = numbers.iter().any { it % 2 == 0 }
// all - checks if all elements satisfy a predicate
let allPositive = numbers.iter().all { it > 0 }
// find - finds the first element matching a predicate
let firstEven = numbers.iter().find { it % 2 == 0 }
// position - finds the index of the first matching element
let evenIndex = numbers.iter().position { it % 2 == 0 }
}
Iterator Adaptors
Iterator adaptors are methods that transform an iterator into a different iterator. You can chain multiple adaptors together because they produce new iterators. However, because iterators are lazy, you must call a consuming adaptor at the end to get results.
map
The map adaptor transforms each element:
fn main() {
let numbers = vec![1, 2, 3]
let squares: Vec<Int> = numbers
.iter()
.map { it * it }
.collect()
println!("Squares: \(squares:?)") // Prints: Squares: [1, 4, 9]
}
filter
The filter adaptor keeps only elements that match a predicate:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let evens: Vec<Int> = numbers
.iter()
.filter { it % 2 == 0 }
.copied() // Convert &Int to Int
.collect()
println!("Evens: \(evens:?)") // Prints: Evens: [2, 4, 6, 8, 10]
}
Chaining Adaptors
The real power comes from chaining multiple adaptors:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Filter evens, square them, keep those under 50
let result: Vec<Int> = numbers
.iter()
.filter { it % 2 == 0 }
.map { it * it }
.filter { it < 50 }
.copied()
.collect()
println!("Result: \(result:?)") // Prints: Result: [4, 16, 36]
}
More Iterator Adaptors
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
// take - takes at most n elements
let first3: Vec<Int> = numbers.iter().take(3).copied().collect()
// skip - skips the first n elements
let last2: Vec<Int> = numbers.iter().skip(3).copied().collect()
// takeWhile - takes while predicate is true
let underFour: Vec<Int> = numbers.iter().takeWhile { it < &4 }.copied().collect()
// skipWhile - skips while predicate is true
let fromFour: Vec<Int> = numbers.iter().skipWhile { it < &4 }.copied().collect()
// enumerate - adds indices
for (index, value) in numbers.iter().enumerate() {
println!("\(index): \(value)")
}
// zip - combines two iterators
let letters = vec!['a', 'b', 'c']
let zipped: Vec<(Int, Char)> = numbers.iter().copied().zip(letters.iter().copied()).collect()
// chain - concatenates two iterators
let more = vec![6, 7, 8]
let all: Vec<Int> = numbers.iter().copied().chain(more.iter().copied()).collect()
// flatten - flattens nested iterators
let nested = vec![vec![1, 2], vec![3, 4], vec![5, 6]]
let flat: Vec<Int> = nested.iter().flatten().copied().collect()
// flatMap - map then flatten
let doubledFlat: Vec<Int> = numbers
.iter()
.copied()
.flatMap { n -> vec![n, n].intoIter() }
.collect()
// rev - reverses the iterator
let reversed: Vec<Int> = numbers.iter().copied().rev().collect()
// cloned/copied - clones/copies the elements
let cloned: Vec<Int> = numbers.iter().cloned().collect()
let copied: Vec<Int> = numbers.iter().copied().collect()
// inspect - peek at values (useful for debugging)
let result: Vec<Int> = numbers
.iter()
.inspect { println!("Before map: \(it)") }
.map { it * 2 }
.inspect { println!("After map: \(it)") }
.collect()
}
Closures That Capture Their Environment
Iterator adaptors that take closures benefit from closures' ability to capture their environment:
#[derive(Debug)]
struct Shoe {
size: Int,
style: String,
}
fn shoesInMySize(shoes: Vec<Shoe>, mySize: Int): Vec<Shoe> {
shoes
.intoIter()
.filter { it.size == mySize } // Captures mySize from environment
.collect()
}
fn main() {
let shoes = vec![
Shoe { size: 10, style: "sneaker".toString() },
Shoe { size: 13, style: "sandal".toString() },
Shoe { size: 10, style: "boot".toString() },
]
let myShoes = shoesInMySize(shoes, 10)
println!("My shoes: \(myShoes:?)")
}
Creating Your Own Iterators
You can create custom iterators by implementing the Iterator trait. You only
need to implement the next method:
struct Counter {
count: Int,
max: Int,
}
extension Counter {
public static fn new(max: Int): Counter {
Counter { count: 0, max: max }
}
}
extension Counter: Iterator {
type Item = Int
mutating fn next(): Int? {
if self.count < self.max {
self.count += 1
Some(self.count)
} else {
null
}
}
}
fn main() {
let counter = Counter.new(5)
for n in counter {
println!("Count: \(n)")
}
// Prints: Count: 1, Count: 2, Count: 3, Count: 4, Count: 5
}
Once you implement next, you get all the other iterator methods for free:
fn main() {
let counter = Counter.new(10)
let sum: Int = counter
.filter { it % 2 == 0 }
.map { it * it }
.sum()
println!("Sum of squares of evens: \(sum)")
// 4 + 16 + 36 + 64 + 100 = 220
}
Using Iterator Methods vs. Loops
Iterator methods can often replace explicit loops with more declarative code. Compare these two approaches:
Using a Loop
fn search(query: &str, contents: &str): Vec<&str> {
var results: Vec<&str> = vec![]
for line in contents.lines() {
if line.contains(query) {
results.push(line)
}
}
results
}
Using Iterators
fn search(query: &str, contents: &str): Vec<&str> {
contents
.lines()
.filter { it.contains(query) }
.collect()
}
The iterator version:
- Has no mutable state (
var results) - Is more declarative (says what to do, not how)
- Is easier to parallelize later (swap
iter()forparIter()with rayon) - Is typically just as fast (see the Performance section)
Common Patterns
Here are some common iterator patterns you'll encounter:
Transforming Collections
fn main() {
let strings = vec!["1", "2", "three", "4", "five"]
// Parse valid numbers, ignore errors
let numbers: Vec<Int> = strings
.iter()
.filterMap { it.parse<Int>().ok() }
.collect()
println!("Numbers: \(numbers:?)") // Prints: Numbers: [1, 2, 4]
}
Finding Elements
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
// Find first even
let firstEven = numbers.iter().find { it % 2 == 0 }
// Find last odd
let lastOdd = numbers.iter().rev().find { it % 2 == 1 }
// Find or default
let target = numbers.iter().find { it > &10 }.copied().unwrapOr(0)
}
Grouping and Partitioning
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Partition into two collections
let (evens, odds): (Vec<Int>, Vec<Int>) = numbers
.iter()
.copied()
.partition { it % 2 == 0 }
println!("Evens: \(evens:?)") // [2, 4, 6, 8, 10]
println!("Odds: \(odds:?)") // [1, 3, 5, 7, 9]
}
Building Strings
fn main() {
let words = vec!["Hello", "World", "from", "Oxide"]
let sentence = words.iter().copied().collect<Vec<&str>>().join(" ")
// Or using fold
let sentence = words
.iter()
.skip(1)
.fold(words[0].toString(), { acc, word -> "\(acc) \(word)" })
println!("\(sentence)") // Hello World from Oxide
}
Numeric Ranges
fn main() {
// Sum of 1 to 100
let sum: Int = (1..=100).sum()
// Squares of 1 to 10
let squares: Vec<Int> = (1..=10).map { it * it }.collect()
// Even numbers from 0 to 20
let evens: Vec<Int> = (0..=20).filter { it % 2 == 0 }.collect()
}
Summary
Iterators in Oxide provide:
- Lazy evaluation: Work is only done when needed
- Composability: Chain multiple operations together
- Clean syntax: Trailing closures with
itmake chains readable - Zero cost: Compiles to efficient code (see next section)
- Rich API: Many built-in adaptors for common operations
Iterators and closures together form a powerful toolkit for processing data in a declarative, efficient way. In the next section, we'll see why you don't have to sacrifice performance for this expressiveness.