Adaptadores de iterador: map, filter, collect, fold

1. Introdução aos Adaptadores de Iterador

Em Rust, os adaptadores de iterador são métodos que transformam ou filtram sequências de dados sem executar a computação imediatamente. Eles seguem o princípio de lazy evaluation: você constrói um pipeline de transformações, mas nada é processado até que um consumidor (consumer) seja chamado.

A diferença fundamental é:
- Adaptadores (lazy): map, filter, fold — retornam um novo iterador que ainda não foi avaliado.
- Consumidores (eager): collect, sum, for_each — disparam a execução do pipeline.

A mágica está no encadeamento de métodos. Você pode construir cadeias complexas como:

let resultado: Vec<i32> = (1..10)
    .filter(|x| x % 2 == 0)
    .map(|x| x * 3)
    .collect();

Isso é mais expressivo e seguro que loops manuais, pois o compilador verifica tipos em cada etapa.

2. map: Transformando Elementos

O adaptador map aplica uma closure a cada elemento do iterador, produzindo um novo iterador com os resultados. Sua assinatura é:

fn map<B, F>(self, f: F) -> Map<Self, F>
where
    F: FnMut(Self::Item) -> B,

A closure pode capturar contexto externo de três formas:

let fator = 3;
let numeros = vec![1, 2, 3];

// Captura por referência (imutável)
let iter1 = numeros.iter().map(|x| x * fator);

// Captura mutável (cuidado com empréstimos)
let mut acumulador = 0;
let iter2 = numeros.iter().map(|x| {
    acumulador += x;
    x * 2
});

// Captura por movimento (`move`)
let nome = String::from("item");
let iter3 = numeros.iter().map(move |x| format!("{}-{}", nome, x));

Exemplo prático: converter strings para números:

let textos = vec!["10", "20", "30", "40"];
let numeros: Vec<i32> = textos
    .iter()
    .map(|s| s.parse::<i32>().unwrap())
    .collect();

println!("{:?}", numeros); // [10, 20, 30, 40]

3. filter: Selecionando Elementos

filter mantém apenas os elementos que satisfazem uma condição booleana:

fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
    P: FnMut(&Self::Item) -> bool,

Note que a closure recebe uma referência ao elemento, não o elemento em si. Exemplo básico:

let numeros = vec![1, 2, 3, 4, 5, 6];
let pares: Vec<i32> = numeros
    .iter()
    .filter(|&&x| x % 2 == 0)
    .copied()
    .collect();

println!("{:?}", pares); // [2, 4, 6]

Combinando filter com map:

let dados = vec![1, 2, 3, 4, 5, 6];
let resultado: Vec<i32> = dados
    .iter()
    .filter(|&&x| x > 2)          // mantém 3, 4, 5, 6
    .map(|&x| x * 10)              // transforma em 30, 40, 50, 60
    .collect();

println!("{:?}", resultado);

Cuidado com capturas mutáveis: closures em filter podem capturar variáveis mutáveis, mas isso pode causar problemas com empréstimos simultâneos:

let mut limite = 3;
let numeros = vec![1, 2, 3, 4, 5];

// Isso compila, mas é frágil:
let filtrados: Vec<i32> = numeros
    .iter()
    .filter(|&&x| {
        let resultado = x > limite;
        if resultado {
            limite += 1; // mutação aqui
        }
        resultado
    })
    .copied()
    .collect();

println!("{:?}", filtrados);

Em geral, prefira closures que não modifiquem estado externo.

4. collect: Coletando Resultados

collect é o consumidor mais versátil: transforma um iterador em qualquer coleção que implemente FromIterator. Sua assinatura:

fn collect<B: FromIterator<Self::Item>>(self) -> B

O tipo de retorno deve ser inferido explicitamente ou anotado:

let v: Vec<i32> = (1..5).collect();           // Vec
let s: HashSet<i32> = (1..5).collect();       // HashSet
let m: String = vec!["a", "b"].into_iter().collect(); // String

Coletando em Result e Option:

let resultados = vec![Ok(1), Ok(2), Ok(3)];
let todos_ok: Result<Vec<i32>, ()> = resultados.into_iter().collect();
assert_eq!(todos_ok, Ok(vec![1, 2, 3]));

let com_erro = vec![Ok(1), Err("falha"), Ok(3)];
let parcial: Result<Vec<i32>, &str> = com_erro.into_iter().collect();
assert_eq!(parcial, Err("falha")); // para no primeiro erro

Para Option:

let alguns: Vec<Option<i32>> = vec![Some(1), Some(2), Some(3)];
let todos: Option<Vec<i32>> = alguns.into_iter().collect();
assert_eq!(todos, Some(vec![1, 2, 3]));

let com_none = vec![Some(1), None, Some(3)];
let resultado: Option<Vec<i32>> = com_none.into_iter().collect();
assert_eq!(resultado, None);

5. fold: Redução e Acumulação

fold é o adaptador que reduz um iterador a um único valor, usando um acumulador. Sua assinatura:

fn fold<B, F>(self, init: B, f: F) -> B
where
    F: FnMut(B, Self::Item) -> B,

Exemplos clássicos:

let numeros = vec![1, 2, 3, 4, 5];

// Soma
let soma = numeros.iter().fold(0, |acc, x| acc + x);
assert_eq!(soma, 15);

// Produto
let produto = numeros.iter().fold(1, |acc, x| acc * x);
assert_eq!(produto, 120);

// Concatenação de strings
let palavras = vec!["Rust", "é", "incrível"];
let frase = palavras.iter().fold(String::new(), |acc, s| {
    if acc.is_empty() { s.to_string() } else { format!("{} {}", acc, s) }
});
assert_eq!(frase, "Rust é incrível");

Comparação com reduce:

// reduce retorna Option (None para iterador vazio)
let vazio: Vec<i32> = vec![];
let resultado_reduce = vazio.iter().reduce(|acc, x| acc + x);
assert_eq!(resultado_reduce, None);

// fold sempre retorna o valor inicial
let resultado_fold = vazio.iter().fold(0, |acc, x| acc + x);
assert_eq!(resultado_fold, 0);

6. Combinações e Encadeamentos Avançados

Pipelines complexos são naturais em Rust:

let dados = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];

let resultado = dados
    .iter()
    .enumerate()           // adiciona índice
    .filter(|&(i, &v)| i % 2 == 0)  // posições pares
    .map(|(_, &v)| v * 2)           // dobra valores
    .fold(0, |acc, v| acc + v);     // soma

println!("Soma dos valores dobrados em posições pares: {}", resultado);

Usando zip com adaptadores:

let nomes = vec!["Alice", "Bob", "Carol"];
let idades = vec![30, 25, 35];

let pessoas: Vec<String> = nomes
    .iter()
    .zip(idades.iter())
    .map(|(nome, idade)| format!("{} tem {} anos", nome, idade))
    .collect();

println!("{:?}", pessoas);

Lazy evaluation: O pipeline só executa quando collect ou outro consumidor é chamado:

let iter = (1..10)
    .filter(|x| {
        println!("Filtrando {}", x);
        x % 2 == 0
    })
    .map(|x| {
        println!("Mapeando {}", x);
        x * 2
    });

// Nada foi impresso ainda!
println!("Pipeline construído");

let resultado: Vec<i32> = iter.collect(); // Agora executa
println!("{:?}", resultado);

Saída:

Pipeline construído
Filtrando 1
Filtrando 2
Mapeando 2
Filtrando 3
Filtrando 4
Mapeando 4
...

7. Tratamento de Erros com Adaptadores

filter_map combina filtragem e transformação em um único passo, usando Option:

let entradas = vec!["1", "2a", "3", "quatro", "5"];

let numeros_validos: Vec<i32> = entradas
    .iter()
    .filter_map(|s| s.parse::<i32>().ok())
    .collect();

println!("{:?}", numeros_validos); // [1, 3, 5]

Trabalhando com Result e propagação de erros:

use std::num::ParseIntError;

fn processar_linhas(linhas: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    linhas
        .iter()
        .map(|s| s.parse::<i32>())
        .collect() // coleta Result<Vec<i32>, ParseIntError>
}

let dados_ok = vec!["10", "20", "30"];
let dados_erro = vec!["10", "vinte", "30"];

println!("{:?}", processar_linhas(&dados_ok));   // Ok([10, 20, 30])
println!("{:?}", processar_linhas(&dados_erro)); // Err(ParseIntError)

Exemplo prático com fallback:

let linhas = vec!["42", "invalido", "100", "erro", "7"];

let processados: Vec<i32> = linhas
    .iter()
    .map(|s| s.parse::<i32>().unwrap_or(0)) // fallback para 0
    .collect();

println!("{:?}", processados); // [42, 0, 100, 0, 7]

8. Considerações Finais e Boas Práticas

Quando usar adaptadores vs loops tradicionais:

  • Use adaptadores para pipelines claros e transformações em sequência.
  • Use loops quando a lógica for muito complexa ou precisar de break/continue não triviais.
  • O compilador otimiza ambos igualmente bem; escolha pela legibilidade.

Evite closures muito complexas:

// Ruim: closure gigante dentro do map
let resultado: Vec<i32> = dados.iter().map(|x| {
    // 20 linhas de lógica aqui
    x * 2 + 3
}).collect();

// Bom: extraia para função nomeada
fn transformar(x: &i32) -> i32 {
    // 20 linhas de lógica aqui
    x * 2 + 3
}

let resultado: Vec<i32> = dados.iter().map(transformar).collect();

Use clippy para sugestões idiomáticas:

cargo clippy

Clippy sugere substituir filter().map() por filter_map(), usar fold em vez de loops manuais, e muito mais.

Os adaptadores de iterador são uma das features mais elegantes de Rust. Eles combinam segurança de tipos, expressividade funcional e performance zero-cost. Domine-os e seu código ficará mais limpo, seguro e idiomático.


Referências

Referências (continuação)

  • Rust Clippy Documentation — Documentação oficial do linter Clippy, com sugestões para escrever código Rust mais idiomático, incluindo recomendações sobre uso de adaptadores de iterador.
  • Rust Performance Book: Iterators — Guia de performance sobre iteradores em Rust, explicando como o compilador otimiza pipelines de adaptadores.

Conclusão

Os adaptadores de iterador em Rust — map, filter, collect, fold e seus companheiros — formam a espinha dorsal da programação funcional na linguagem. Eles permitem transformar, filtrar, acumular e coletar dados de forma declarativa, segura e eficiente, sem sacrificar performance.

A lazy evaluation garante que apenas o necessário seja computado, enquanto a inferência de tipos e o sistema de ownership tornam o código robusto contra erros comuns. Combinados com closures, esses adaptadores criam pipelines expressivos que substituem loops verbosos por cadeias de métodos legíveis e modulares.

Lembre-se das boas práticas:
- Prefira filter_map a filter().map() quando possível
- Extraia closures complexas para funções nomeadas
- Use clippy para manter seu código idiomático
- Escolha entre adaptadores e loops baseado na clareza, não na performance (o compilador otimiza ambos)

Com esses conceitos dominados, você está preparado para escrever código Rust mais elegante, funcional e eficiente. Aproveite o poder dos iteradores!