Iteradores são um dos pilares da programação funcional e imperativa em Rust. Eles permitem percorrer sequências de dados de forma eficiente e expressiva, sem a necessidade de loops manuais. Nesta aula, exploraremos o trait Iterator, seu método fundamental next, os adaptadores mais comuns (map, filter) e o conceito de lazy evaluation, que é crucial para o desempenho.

Dominar iteradores é essencial para escrever código idiomático em Rust, pois eles são amplamente utilizados em coleções, I/O e processamento de dados. Vamos começar com a base: o trait Iterator.

trait Iterator

Em Rust, um iterador é qualquer tipo que implementa o trait Iterator. Este trait exige a definição de um método next que retorna Option<Self::Item>. O trait também fornece dezenas de métodos padrão (adaptadores e consumidores) que são implementados automaticamente para qualquer tipo que implemente Iterator.

A definição simplificada do trait é:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // muitos métodos padrão
}

O tipo associado Item é o tipo dos elementos que o iterador produz. O método next avança o iterador e retorna o próximo elemento, ou None quando a iteração termina.

Qualquer tipo que implemente Iterator automaticamente ganha acesso a métodos como map, filter, fold, collect, entre outros. Isso torna o código muito mais conciso e legível.

next

O método next é o coração de qualquer iterador. Ele é chamado repetidamente para obter os elementos um a um. Quando o iterador se esgota, next retorna None.

Vamos implementar um iterador simples que conta de 1 até um limite:

struct Counter {
    count: u32,
    limit: u32,
}

impl Counter {
    fn new(limit: u32) -> Counter {
        Counter { count: 0, limit }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.limit {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter::new(5);
    println!("{:?}", counter.next()); // Some(1)
    println!("{:?}", counter.next()); // Some(2)
    println!("{:?}", counter.next()); // Some(3)
    println!("{:?}", counter.next()); // Some(4)
    println!("{:?}", counter.next()); // Some(5)
    println!("{:?}", counter.next()); // None
}

Note que o iterador é mutável: next recebe &mut self. Isso porque ele precisa modificar seu estado interno para rastrear a posição atual.

Na prática, raramente chamamos next diretamente. Em vez disso, usamos loops for que chamam next automaticamente até None:

for number in Counter::new(5) {
    println!("{}", number);
}

O loop for consome o iterador, chamando next a cada iteração.

Adaptadores (map, filter)

Adaptadores são métodos do trait Iterator que transformam um iterador em outro iterador. Eles não consomem o iterador original, mas produzem um novo que aplica uma transformação a cada elemento. Os adaptadores mais comuns são map e filter.

map: aplica uma função a cada elemento, produzindo um novo iterador com os resultados. Por exemplo, para elevar ao quadrado cada número:

let squares = (1..=5).map(|x| x * x);
for s in squares {
    println!("{}", s); // 1, 4, 9, 16, 25
}

filter: seleciona apenas os elementos que satisfazem uma condição (função que retorna bool). Exemplo: filtrar números pares:

let evens = (1..=10).filter(|x| x % 2 == 0);
for e in evens {
    println!("{}", e); // 2, 4, 6, 8, 10
}

Adaptadores podem ser encadeados para criar pipelines complexos. Por exemplo, para obter os quadrados dos números ímpares entre 1 e 10:

let result: Vec<_> = (1..=10)
    .filter(|x| x % 2 != 0)
    .map(|x| x * x)
    .collect();
println!("{:?}", result); // [1, 9, 25, 49, 81]

Note que usamos collect para consumir o iterador e produzir um vetor. Sem um método consumidor, o iterador nunca é executado.

Lazy evaluation

Uma das características mais importantes dos iteradores em Rust é a lazy evaluation (avaliação preguiçosa). Isso significa que os adaptadores não executam nenhum trabalho até que um método consumidor seja chamado. Eles apenas definem uma transformação que será aplicada quando necessário.

Por exemplo, considere:

let iter = (1..=100).filter(|x| {
    println!("filtrando {}", x);
    x % 2 == 0
}).map(|x| {
    println!("mapeando {}", x);
    x * x
});
println!("Ainda não processou nada");
let first = iter.next(); // apenas aqui começa o processamento
println!("Primeiro: {:?}", first);

A saída será:

Ainda não processou nada
filtrando 1
filtrando 2
mapeando 2
Primeiro: Some(4)

Observe que o processamento só ocorre quando chamamos next. Além disso, o iterador é preguiçoso e só processa o necessário para produzir o próximo elemento. Isso permite trabalhar com sequências infinitas ou grandes volumes de dados sem consumir memória desnecessária.

A lazy evaluation também evita alocações intermediárias: cada adaptador é uma estrutura que envolve o iterador anterior, e os elementos são transformados um a um sob demanda.

Para consumir um iterador, usamos métodos como collect, fold, sum, count, for_each, entre outros. Eles forçam a iteração completa.

Boas práticas: prefira encadear adaptadores e usar um consumidor no final. Evite coletar desnecessariamente em coleções intermediárias, pois isso pode prejudicar o desempenho.

Referências

Exercícios

  1. Implemente um iterador personalizado chamado Fibonacci que gera números de Fibonacci até um limite máximo. Use o trait Iterator.

    ✓ Resposta:
    struct Fibonacci {
        curr: u64,
        next: u64,
        max: u64,
    }
    
    impl Fibonacci {
        fn new(max: u64) -> Self {
            Fibonacci { curr: 0, next: 1, max }
        }
    }
    
    impl Iterator for Fibonacci {
        type Item = u64;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.curr > self.max {
                None
            } else {
                let result = self.curr;
                let new_next = self.curr + self.next;
                self.curr = self.next;
                self.next = new_next;
                Some(result)
            }
        }
    }
    
    fn main() {
        let fib = Fibonacci::new(100);
        for num in fib {
            println!("{}", num);
        }
    }
  2. Use o adaptador map para converter uma lista de temperaturas em Celsius para Fahrenheit. Dado um vetor celsius = vec![0.0, 10.0, 20.0, 30.0, 40.0], produza um vetor com as temperaturas em Fahrenheit (fórmula: F = C * 9/5 + 32).

    ✓ Resposta:
    fn main() {
        let celsius = vec![0.0, 10.0, 20.0, 30.0, 40.0];
        let fahrenheit: Vec<f64> = celsius.iter()
            .map(|c| c * 9.0 / 5.0 + 32.0)
            .collect();
        println!("{:?}", fahrenheit); // [32.0, 50.0, 68.0, 86.0, 104.0]
    }
  3. Escreva uma expressão com iteradores que encontre a soma dos quadrados dos números pares entre 1 e 20.

    ✓ Resposta:
    fn main() {
        let sum: u32 = (1..=20)
            .filter(|x| x % 2 == 0)
            .map(|x| x * x)
            .sum();
        println!("{}", sum); // 2^2 + 4^2 + ... + 20^2 = 1540
    }
  4. Explique por que o código abaixo imprime apenas "um" e "dois" e não os números após o panic. O que a lazy evaluation tem a ver com isso?

    fn main() {
        let iter = vec!["um", "dois", "tres", "quatro"]
            .into_iter()
            .inspect(|x| println!("Vendo: {}", x))
            .take(2);
        for i in iter {
            println!("{}", i);
        }
    }

    ✓ Resposta:O adaptador take(2) limita o iterador a apenas 2 elementos. Devido à lazy evaluation, o iterador só processa os elementos necessários para satisfazer a demanda. Quando o loop for pede o terceiro elemento, o take já retornou None após o segundo, então inspect nunca é chamado para os elementos restantes. Portanto, apenas "um" e "dois" são impressos.
  5. Crie um iterador infinito que gere números ímpares (1, 3, 5, ...) e use take para coletar os 10 primeiros em um vetor.

    ✓ Resposta:
    fn main() {
        let odds: Vec<u32> = (1..).filter(|x| x % 2 != 0).take(10).collect();
        println!("{:?}", odds); // [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
    }