Iteradores são um dos pilares da programação funcional em Rust. Eles permitem processar sequências de elementos de forma declarativa, segura e eficiente. Nesta aula, exploraremos o trait Iterator, seu método fundamental next, os adaptadores mais comuns (map, filter) e o conceito de avaliação preguiçosa (lazy evaluation), que torna os iteradores tão performáticos.

Dominar iteradores é essencial para escrever código Rust idiomático e de alto desempenho. A biblioteca padrão faz uso intenso de iteradores, e você os encontrará em manipulação de coleções, I/O, parsing e muito mais.

trait Iterator

O trait Iterator é definido na biblioteca padrão e fornece uma interface para tipos que podem produzir uma sequência de valores. Ele exige a implementação de um único método: next, que retorna Option<Self::Item>. Quando o iterador termina, next retorna None.

Implementar o trait Iterator manualmente permite criar iteradores personalizados. Por exemplo, um contador que gera números de 1 a N:

struct Contador {
    max: u32,
    atual: u32,
}

impl Contador {
    fn new(max: u32) -> Self {
        Contador { max, atual: 0 }
    }
}

impl Iterator for Contador {
    type Item = u32;

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

fn main() {
    let mut c = Contador::new(5);
    while let Some(val) = c.next() {
        println!("{}", val);
    }
}

O trait Iterator também fornece dezenas de métodos padrão (chamados adaptadores) que são implementados automaticamente para qualquer tipo que implemente next. Isso inclui map, filter, fold, collect, entre outros.

next

O método next é o coração de todo iterador. Ele avança o iterador e retorna o próximo elemento, ou None se a iteração terminou. Chamar next repetidamente permite percorrer a sequência manualmente, mas geralmente usamos loops for ou adaptadores que chamam next internamente.

O loop for em Rust é syntactic sugar para uma chamada repetida a next até que retorne None. Por exemplo:

let v = vec![1, 2, 3];
for i in v.iter() {
    println!("{}", i);
}
// Equivalente a:
let mut it = v.iter();
while let Some(i) = it.next() {
    println!("{}", i);
}

Note que v.iter() retorna um iterador sobre referências (&i32). Existem também iter_mut() para referências mutáveis e into_iter() para iteradores que consomem o valor (movendo os elementos).

Adaptadores (map, filter)

Adaptadores são métodos do trait Iterator que transformam um iterador em outro iterador. Eles são lazy, ou seja, não executam a transformação até que um consumidor (como collect ou for) force a iteração.

map: aplica uma função a cada elemento, produzindo um novo iterador com os resultados.

let v = vec![1, 2, 3];
let quadrados: Vec<i32> = v.iter().map(|x| x * x).collect();
println!("{:?}", quadrados); // [1, 4, 9]

filter: seleciona elementos que satisfazem um predicado (função que retorna bool).

let v = vec![1, 2, 3, 4, 5];
let pares: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", pares); // [2, 4]

Adaptadores podem ser encadeados para criar pipelines complexas de forma legível:

let v = vec![1, 2, 3, 4, 5];
let resultado: Vec<i32> = v.iter()
    .filter(|&&x| x > 2)
    .map(|x| x * 10)
    .collect();
println!("{:?}", resultado); // [30, 40, 50]

Outros adaptadores comuns incluem take, skip, enumerate, zip, chain, flat_map, etc.

Lazy evaluation

Lazy evaluation (avaliação preguiçosa) significa que as transformações em um iterador só são executadas quando necessário. Em Rust, os adaptadores de iterador são lazy por padrão. Isso permite construir pipelines complexas sem custo até o momento do consumo.

Por exemplo, o código a seguir não imprime nada, porque map é lazy e collect não é chamado:

let v = vec![1, 2, 3];
v.iter().map(|x| {
    println!("processando {}", x);
    x * 2
});
// Nada é impresso

Para forçar a execução, precisamos consumir o iterador com collect, for_each, ou um loop for:

let v = vec![1, 2, 3];
let resultado: Vec<i32> = v.iter().map(|x| {
    println!("processando {}", x);
    x * 2
}).collect();
// Imprime:
// processando 1
// processando 2
// processando 3

Lazy evaluation traz vantagens de performance: se você usar take(2) em um iterador infinito, apenas dois elementos são processados. Também permite trabalhar com sequências potencialmente infinitas, como números aleatórios ou leitura de um arquivo.

Entretanto, é preciso cuidado com efeitos colaterais: closures dentro de adaptadores lazy podem não ser executadas se o iterador não for consumido. Por isso, é idiomático manter as closures sem efeitos colaterais (puramente funcionais) ou usar for_each quando efeitos são necessários.

Boas práticas:

  • Prefira encadear adaptadores a escrever loops explícitos – o código fica mais declarativo e menos propenso a erros.
  • Use collect para materializar o resultado em um vetor, mas lembre-se de que isso aloca memória. Para operações curtas, pode ser aceitável; para grandes volumes, considere processar elemento a elemento.
  • Evite closures que capturem muitas variáveis ou tenham efeitos colaterais – isso pode dificultar a compreensão e prejudicar a lazy evaluation.

Referências

Exercícios

  1. Crie um iterador personalizado chamado Fibonacci que gere os números da sequência de Fibonacci infinitamente. Implemente o trait Iterator para ele. Dica: use dois campos a e b para os dois últimos valores.

    ✓ Resposta:
    struct Fibonacci {
        a: u64,
        b: u64,
    }
    
    impl Fibonacci {
        fn new() -> Self {
            Fibonacci { a: 0, b: 1 }
        }
    }
    
    impl Iterator for Fibonacci {
        type Item = u64;
    
        fn next(&mut self) -> Option<Self::Item> {
            let next = self.a;
            self.a = self.b;
            self.b = next + self.b;
            Some(next)
        }
    }
    
    fn main() {
        let fib = Fibonacci::new();
        for (i, val) in fib.take(10).enumerate() {
            println!("Fib[{}] = {}", i, val);
        }
    }
  2. Usando adaptadores, escreva uma expressão que, dada uma slice de inteiros, retorne um vetor contendo apenas os números ímpares, multiplicados por 3.

    ✓ Resposta:
    let numeros = [1, 2, 3, 4, 5, 6];
    let resultado: Vec<i32> = numeros.iter()
        .filter(|&&x| x % 2 != 0)
        .map(|&x| x * 3)
        .collect();
    println!("{:?}", resultado); // [3, 9, 15]
  3. Explique por que o código a seguir não imprime nada e corrija-o para que imprima os números de 1 a 5.

    (1..=5).map(|x| println!("{}", x));

    ✓ Resposta:

    O map é lazy; a closure só é executada quando o iterador é consumido. Para imprimir, use for_each ou um loop for:

    (1..=5).for_each(|x| println!("{}", x));
    // Ou:
    for x in 1..=5 {
        println!("{}", x);
    }
  4. Dado um vetor de strings, use adaptadores para concatenar todas as strings em uma única string, separadas por vírgula. Exemplo: ["a", "b", "c"]"a,b,c".

    ✓ Resposta:
    let palavras = vec!["a", "b", "c"];
    let resultado = palavras.iter()
        .map(|s| s.as_str())
        .collect::<Vec<&str>>()
        .join(",");
    println!("{}", resultado); // "a,b,c"
    
    // Ou mais diretamente:
    let resultado2 = palavras.join(",");
    println!("{}", resultado2); // "a,b,c"
  5. Implemente um iterador MeuIterador sobre uma struct Pessoa que tenha campos nome e idade. O iterador deve retornar apenas os nomes das pessoas com idade maior que 18. Use um vetor de pessoas e, em seguida, use filter_map para obter um iterador de &str.

    ✓ Resposta:
    struct Pessoa {
        nome: String,
        idade: u8,
    }
    
    fn main() {
        let pessoas = vec![
            Pessoa { nome: "Alice".to_string(), idade: 25 },
            Pessoa { nome: "Bob".to_string(), idade: 17 },
            Pessoa { nome: "Carol".to_string(), idade: 30 },
        ];
    
        let adultos: Vec<&str> = pessoas.iter()
            .filter_map(|p| {
                if p.idade > 18 {
                    Some(p.nome.as_str())
                } else {
                    None
                }
            })
            .collect();
        println!("{:?}", adultos); // ["Alice", "Carol"]
    }