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:

MethodProducesUse 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() for parIter() 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 it make 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.