Iterator Pattern em Rust: Iteração Lazy, Composável e Idiomática

Guia completo do padrão Iterator em Rust: trait Iterator, iteradores customizados, combinadores map/filter/fold, IntoIterator, avaliação lazy e exemplos práticos.

O Iterator Pattern (Padrão de Iterador) permite percorrer elementos de uma coleção sem expor sua representação interna. Em Rust, este padrão não é apenas um design pattern — é uma peça central da linguagem, integrado profundamente na biblioteca padrão e no compilador.

A trait Iterator de Rust vai muito além do que GoF imaginava: ela oferece avaliação lazy (preguiçosa), dezenas de combinadores composáveis (map, filter, fold, collect), e graças a monomorphization, iteradores em Rust compilam para código tão eficiente quanto loops manuais — são uma verdadeira abstração de custo zero.

Problema

Ao trabalhar com coleções de dados, surgem desafios recorrentes:

  • Como percorrer diferentes estruturas de dados com uma interface uniforme?
  • Como compor transformações sem criar coleções intermediárias?
  • Como processar grandes conjuntos de dados sem carregar tudo na memória?
  • Como permitir que o consumidor controle o ritmo da iteração?

Sem iteradores, cada estrutura de dados precisaria de sua própria API de travessia, e composição de operações exigiria vetores temporários para cada passo intermediário.

Solução em Rust

A Trait Iterator

O coração do padrão em Rust é a trait Iterator:

// Definição simplificada da trait Iterator da biblioteca padrão
trait Iterator {
    type Item;  // Tipo dos elementos produzidos

    // Único método obrigatório
    fn next(&mut self) -> Option<Self::Item>;

    // Dezenas de métodos fornecidos automaticamente:
    // map, filter, fold, collect, enumerate, zip, take, skip...
}

Implementando um Iterador Customizado

Vamos criar um iterador que gera a sequência de Fibonacci:

/// Iterador que gera números de Fibonacci infinitamente
struct Fibonacci {
    atual: u64,
    proximo: u64,
}

impl Fibonacci {
    fn novo() -> Self {
        Fibonacci {
            atual: 0,
            proximo: 1,
        }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let resultado = self.atual;
        self.atual = self.proximo;
        self.proximo = resultado + self.proximo;
        Some(resultado) // Nunca retorna None — é infinito!
    }
}

fn main() {
    // Pegar os 10 primeiros números de Fibonacci
    let fibs: Vec<u64> = Fibonacci::novo().take(10).collect();
    println!("Fibonacci: {:?}", fibs);
    // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    // Soma dos Fibonacci menores que 1000
    let soma: u64 = Fibonacci::novo()
        .take_while(|&n| n < 1000)
        .sum();
    println!("Soma dos Fibonacci < 1000: {}", soma);

    // Fibonacci pares menores que 4_000_000
    let soma_pares: u64 = Fibonacci::novo()
        .take_while(|&n| n < 4_000_000)
        .filter(|n| n % 2 == 0)
        .sum();
    println!("Soma dos Fibonacci pares < 4M: {}", soma_pares);
}

Combinadores Essenciais

fn demonstrar_combinadores() {
    let numeros = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // map: transforma cada elemento
    let dobrados: Vec<i32> = numeros.iter().map(|n| n * 2).collect();
    println!("Dobrados: {:?}", dobrados);

    // filter: seleciona elementos que satisfazem um predicado
    let pares: Vec<&i32> = numeros.iter().filter(|n| *n % 2 == 0).collect();
    println!("Pares: {:?}", pares);

    // fold: acumula todos os elementos em um único valor
    let soma = numeros.iter().fold(0, |acumulador, &n| acumulador + n);
    println!("Soma: {}", soma);

    // enumerate: adiciona índice a cada elemento
    for (indice, valor) in numeros.iter().enumerate() {
        if indice < 3 {
            println!("  [{}] = {}", indice, valor);
        }
    }

    // zip: combina dois iteradores em pares
    let nomes = vec!["Alice", "Bob", "Carol"];
    let idades = vec![30, 25, 28];
    let pessoas: Vec<_> = nomes.iter().zip(idades.iter()).collect();
    println!("Pessoas: {:?}", pessoas);

    // chain: concatena dois iteradores
    let a = vec![1, 2, 3];
    let b = vec![4, 5, 6];
    let todos: Vec<&i32> = a.iter().chain(b.iter()).collect();
    println!("Encadeados: {:?}", todos);

    // flat_map: mapeia e achata resultados
    let frases = vec!["olá mundo", "rust é incrível"];
    let palavras: Vec<&str> = frases.iter().flat_map(|f| f.split_whitespace()).collect();
    println!("Palavras: {:?}", palavras);

    // windows e chunks (em slices)
    let dados = [1, 2, 3, 4, 5];
    let medias_moveis: Vec<f64> = dados.windows(3)
        .map(|janela| janela.iter().sum::<i32>() as f64 / 3.0)
        .collect();
    println!("Médias móveis: {:?}", medias_moveis);
}

A Trait IntoIterator

A trait IntoIterator permite que qualquer tipo seja usado em um loop for:

/// Coleção customizada que implementa IntoIterator
struct ListaTarefas {
    tarefas: Vec<String>,
}

impl ListaTarefas {
    fn nova() -> Self {
        ListaTarefas { tarefas: Vec::new() }
    }

    fn adicionar(&mut self, tarefa: &str) {
        self.tarefas.push(tarefa.to_string());
    }
}

/// Permite iterar consumindo a lista
impl IntoIterator for ListaTarefas {
    type Item = String;
    type IntoIter = std::vec::IntoIter<String>;

    fn into_iter(self) -> Self::IntoIter {
        self.tarefas.into_iter()
    }
}

/// Permite iterar por referência
impl<'a> IntoIterator for &'a ListaTarefas {
    type Item = &'a String;
    type IntoIter = std::slice::Iter<'a, String>;

    fn into_iter(self) -> Self::IntoIter {
        self.tarefas.iter()
    }
}

fn main() {
    let mut lista = ListaTarefas::nova();
    lista.adicionar("Estudar Rust");
    lista.adicionar("Escrever testes");
    lista.adicionar("Fazer code review");

    // Graças a IntoIterator, funciona no for
    for tarefa in &lista {
        println!("- {}", tarefa);
    }

    // Consumindo a lista
    let todas: Vec<String> = lista.into_iter().collect();
    println!("Total: {} tarefas", todas.len());
}

Diagrama

    Avaliação Lazy — Nenhuma coleção intermediária é criada:

    vec![1,2,3,4,5,6,7,8,9,10]
          │
          ▼ .iter()
    ┌─────────────┐
    │  Iter<i32>  │ ── produz &i32 sob demanda
    └──────┬──────┘
           ▼ .filter(|n| *n % 2 == 0)
    ┌─────────────┐
    │   Filter    │ ── pula ímpares, passa pares
    └──────┬──────┘
           ▼ .map(|n| n * 10)
    ┌─────────────┐
    │     Map     │ ── multiplica por 10
    └──────┬──────┘
           ▼ .take(3)
    ┌─────────────┐
    │    Take     │ ── para após 3 elementos
    └──────┬──────┘
           ▼ .collect()
    ┌─────────────┐
    │ Vec<i32>    │ ── [20, 40, 60]
    └─────────────┘

    Apenas 6 chamadas a next() foram feitas no total!
    Os elementos 8, 9, 10 nunca foram visitados.

    Trait IntoIterator — Três formas de iterar:

    ┌──────────────┐     ┌─────────────────────┐
    │ for x in v   │ ──▶ │ v.into_iter()       │ ── consome v
    │ for x in &v  │ ──▶ │ (&v).into_iter()    │ ── empresta v
    │ for x in &mut v│──▶│ (&mut v).into_iter()│ ── empresta mut
    └──────────────┘     └─────────────────────┘

Exemplo do Mundo Real

Um iterador customizado para percorrer resultados paginados de um banco de dados:

use std::collections::VecDeque;

/// Representa um registro do banco de dados
#[derive(Debug, Clone)]
struct Registro {
    id: u64,
    nome: String,
    valor: f64,
}

/// Simula uma conexão com banco de dados
struct ConexaoBD {
    // Em produção, seria uma conexão real (diesel, sqlx, etc.)
    dados: Vec<Registro>,
}

impl ConexaoBD {
    fn nova_com_dados(dados: Vec<Registro>) -> Self {
        ConexaoBD { dados }
    }

    /// Busca uma página de resultados (simulado)
    fn buscar_pagina(&self, offset: usize, limite: usize) -> Vec<Registro> {
        self.dados.iter()
            .skip(offset)
            .take(limite)
            .cloned()
            .collect()
    }

    fn total_registros(&self) -> usize {
        self.dados.len()
    }
}

/// Iterador que busca resultados do banco em páginas sob demanda
struct IteradorResultados<'a> {
    conexao: &'a ConexaoBD,
    tamanho_pagina: usize,
    offset_atual: usize,
    buffer: VecDeque<Registro>,
    total: usize,
    registros_entregues: usize,
}

impl<'a> IteradorResultados<'a> {
    fn novo(conexao: &'a ConexaoBD, tamanho_pagina: usize) -> Self {
        let total = conexao.total_registros();
        IteradorResultados {
            conexao,
            tamanho_pagina,
            offset_atual: 0,
            buffer: VecDeque::new(),
            total,
            registros_entregues: 0,
        }
    }

    /// Carrega a próxima página no buffer interno
    fn carregar_proxima_pagina(&mut self) {
        let pagina = self.conexao.buscar_pagina(
            self.offset_atual,
            self.tamanho_pagina,
        );
        self.offset_atual += pagina.len();
        for registro in pagina {
            self.buffer.push_back(registro);
        }
    }
}

impl<'a> Iterator for IteradorResultados<'a> {
    type Item = Registro;

    fn next(&mut self) -> Option<Self::Item> {
        // Se o buffer está vazio e ainda há registros, carrega mais
        if self.buffer.is_empty() && self.registros_entregues < self.total {
            self.carregar_proxima_pagina();
        }

        // Retorna o próximo registro do buffer
        let registro = self.buffer.pop_front()?;
        self.registros_entregues += 1;
        Some(registro)
    }

    // Dica de tamanho para otimizar collect()
    fn size_hint(&self) -> (usize, Option<usize>) {
        let restante = self.total - self.registros_entregues;
        (restante, Some(restante))
    }
}

/// Extensão da conexão para criar iteradores facilmente
impl ConexaoBD {
    fn iterar(&self, tamanho_pagina: usize) -> IteradorResultados {
        IteradorResultados::novo(self, tamanho_pagina)
    }
}

fn main() {
    // Criando dados de exemplo
    let dados: Vec<Registro> = (1..=100)
        .map(|i| Registro {
            id: i,
            nome: format!("Produto {}", i),
            valor: i as f64 * 9.99,
        })
        .collect();

    let bd = ConexaoBD::nova_com_dados(dados);

    // Iterando com páginas de 10 registros
    // O banco é consultado sob demanda!
    let caros: Vec<Registro> = bd.iterar(10)
        .filter(|r| r.valor > 500.0)
        .take(5)
        .collect();

    println!("5 produtos caros (>500):");
    for reg in &caros {
        println!("  {} - {} - R${:.2}", reg.id, reg.nome, reg.valor);
    }

    // Cálculo de estatísticas com iteradores
    let (total, soma) = bd.iterar(20)
        .fold((0u64, 0.0f64), |(cnt, soma), r| (cnt + 1, soma + r.valor));
    println!("\nTotal: {} registros, Soma: R${:.2}", total, soma);
    println!("Média: R${:.2}", soma / total as f64);
}

Quando Usar

  • Coleções customizadas: Qualquer estrutura de dados que contenha elementos
  • Processamento em pipeline: Encadear transformações sem alocações intermediárias
  • Dados paginados ou em streaming: Buscar dados sob demanda de fontes externas
  • Sequências infinitas: Fibonacci, números aleatórios, leituras de sensor
  • Redução de dados: Agregação com fold, sum, count, min, max
  • Interoperabilidade: IntoIterator permite que seus tipos funcionem em for

Quando NÃO Usar

  • Acesso aleatório necessário: Iteradores são sequenciais; use slices ou IndexMut
  • Iteração bidirecional frequente: DoubleEndedIterator existe, mas nem sempre é prático
  • Estado compartilhado complexo: Se cada passo depende de estado externo mutável, closures em combinadores ficam difíceis
  • Depuração difícil: Cadeias longas de combinadores podem ser difíceis de depurar — quebre em etapas

Variações em Rust

DoubleEndedIterator

Iteradores que podem ser consumidos de ambas as extremidades:

fn demonstrar_dupla_ponta() {
    let nums = vec![1, 2, 3, 4, 5];

    // .rev() inverte a iteração
    let invertido: Vec<&i32> = nums.iter().rev().collect();
    println!("Invertido: {:?}", invertido);

    // Processar das duas pontas simultaneamente
    let mut iter = nums.iter();
    println!("Frente: {:?}", iter.next());       // Some(1)
    println!("Trás: {:?}", iter.next_back());    // Some(5)
    println!("Frente: {:?}", iter.next());       // Some(2)
    println!("Trás: {:?}", iter.next_back());    // Some(4)
}

ExactSizeIterator

Iteradores que sabem seu tamanho exato, otimizando collect:

struct Intervalo {
    atual: i32,
    fim: i32,
}

impl Iterator for Intervalo {
    type Item = i32;
    fn next(&mut self) -> Option<i32> {
        if self.atual < self.fim {
            let val = self.atual;
            self.atual += 1;
            Some(val)
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let restante = (self.fim - self.atual) as usize;
        (restante, Some(restante))
    }
}

impl ExactSizeIterator for Intervalo {
    fn len(&self) -> usize {
        (self.fim - self.atual) as usize
    }
}

Iteradores com Iterator::scan para estado acumulado

fn demonstrar_scan() {
    // scan mantém estado entre iterações
    let saldo_acumulado: Vec<f64> = vec![100.0, -30.0, 50.0, -10.0, 200.0]
        .into_iter()
        .scan(0.0f64, |saldo, transacao| {
            *saldo += transacao;
            Some(*saldo)
        })
        .collect();

    println!("Saldo acumulado: {:?}", saldo_acumulado);
    // [100.0, 70.0, 120.0, 110.0, 310.0]
}

Padrões Relacionados

  • Visitor: Visitor percorre estruturas com duplo despacho; Iterator percorre sequencialmente
  • Composite: Estruturas compostas frequentemente expõem iteradores para travessia
  • Strategy: Combinadores como map e filter são essencialmente strategies aplicadas a elementos
  • Builder: A cadeia de combinadores do iterador lembra o padrão builder

Conclusão

O Iterator Pattern em Rust transcende o padrão GoF original. A trait Iterator da biblioteca padrão, com seus mais de 70 métodos fornecidos automaticamente, a avaliação lazy que evita alocações intermediárias e a monomorphization que elimina overhead de abstração fazem dos iteradores Rust uma das ferramentas mais poderosas da linguagem.

Dominar iteradores é essencial para escrever código Rust idiomático. A combinação de map, filter, fold e collect cobre a maioria dos cenários de processamento de dados, e a implementação de Iterator e IntoIterator para tipos customizados é uma das habilidades mais valorizadas na comunidade Rust.