Iteradores lazy vs eager
1. Introdução ao Conceito de Lazy e Eager
A avaliação lazy (preguiçosa) ocorre quando um cálculo é adiado até que seu resultado seja realmente necessário. Já a avaliação eager (ansiosa) executa toda a computação imediatamente, independentemente de o resultado ser usado ou não.
Imagine uma linha de produção em uma fábrica. No modo lazy, cada operação só processa um item quando o próximo estágio solicita — como um sistema just-in-time. No modo eager, toda a produção é concluída antes do início da próxima etapa. Rust adota iteradores lazy como padrão porque isso permite compor pipelines eficientes que processam apenas o necessário, sem alocações desnecessárias ou trabalho desperdiçado.
2. O Comportamento Lazy dos Iteradores em Rust
O trait Iterator define o método next(), que retorna Option<Item>. Cada chamada consome um elemento sob demanda. A verdadeira mágica está em como os adaptadores de iterador são projetados: eles nunca executam nada até que alguém chame next().
// Nada é executado aqui — apenas criamos um pipeline lazy
let pipeline = (1..10).map(|x| {
println!("Processando {}", x);
x * 2
});
println!("Pipeline criado, mas nenhum elemento foi processado ainda!");
Para verificar esse comportamento, usamos inspect, que permite observar elementos sem interferir no fluxo:
let numbers = vec![1, 2, 3];
let pipeline = numbers.iter()
.inspect(|x| println!("Elemento visto: {}", x))
.map(|x| x * 10);
println!("Nada foi impresso ainda!");
// Apenas quando consumimos:
let result: Vec<_> = pipeline.collect();
println!("Resultado: {:?}", result);
3. Operações Eager: Consumindo o Iterador
Para realmente executar o pipeline, precisamos de consumidores eager. O mais comum é collect(), que força toda a cadeia de iteração:
let nums: Vec<_> = (1..5).map(|x| {
println!("Map executado para {}", x);
x * 2
}).collect();
// Saída: Map executado para 1, Map executado para 2, ...
Outros consumidores eager importantes:
let sum: i32 = (1..100).sum(); // Soma todos os elementos
let count = (1..100).count(); // Conta todos os elementos
let product: i32 = (1..6).fold(1, |acc, x| acc * x); // 5!
A diferença é clara: sem collect, nada acontece; com collect, todo o pipeline é executado.
4. Adaptadores Lazy vs Consumidores Eager: A Fronteira
A regra de ouro é simples: adaptadores lazy retornam outro Iterator, enquanto consumidores eager retornam valores concretos (como Vec<T>, i32, bool).
Adaptadores lazy:
let lazy = (0..10)
.map(|x| x * 2) // lazy
.filter(|x| x % 3 == 0) // lazy
.take(3) // lazy
.skip(1); // lazy
// Ainda nada foi executado!
Consumidores eager:
let eager: Vec<_> = (0..10)
.map(|x| x * 2)
.filter(|x| x % 3 == 0)
.take(3)
.skip(1)
.collect(); // eager! Agora sim executa tudo
Outros consumidores eager: find, any, all, nth, last, position.
5. Implicações de Performance e Memória
A maior vantagem do lazy é processar apenas o necessário. Considere:
let resultado: Vec<_> = (0..1_000_000)
.filter(|x| x % 1000 == 0)
.take(3)
.collect();
Aqui, apenas 3 elementos são processados, não o milhão inteiro. Isso economiza memória e tempo de CPU. Se fosse eager, um vetor com 1.000 elementos filtrados seria alocado para depois pegar apenas 3.
Mas cuidado com side effects em closures lazy:
let mut contador = 0;
let pipeline = (0..10).map(|x| {
contador += 1; // Isso só executa quando consumido!
x
});
println!("Contador ainda é {}", contador); // 0
pipeline.for_each(|_| {});
println!("Contador agora é {}", contador); // 10
6. Iteradores Eager por Construção: Vec e Coleções
Coleções como Vec têm métodos que consomem iteradores de forma eager:
let mut vec = vec![1, 2, 3];
vec.extend([4, 5, 6]); // eager: adiciona todos os elementos imediatamente
let from_iter: Vec<_> = Vec::from_iter(0..5); // eager
Um loop for é syntactic sugar para iteração eager:
// for loop é eager
for x in vec.iter().map(|x| x * 2) {
println!("{}", x); // executa imediatamente
}
// Equivalente a:
let mut iter = vec.iter().map(|x| x * 2);
while let Some(x) = iter.next() {
println!("{}", x);
}
7. Combinando Lazy e Eager na Prática
O padrão mais comum é construir um pipeline lazy e finalizar com um consumidor eager:
use std::fs::File;
use std::io::{BufRead, BufReader};
let file = File::open("dados.txt").unwrap();
let leitor = BufReader::new(file);
let resultado: Vec<String> = leitor
.lines() // lazy: iterator de Result<String>
.filter_map(|line| line.ok()) // lazy: ignora erros
.map(|line| line.trim().to_string()) // lazy
.filter(|line| !line.is_empty()) // lazy
.take(100) // lazy: só 100 linhas
.collect(); // eager: força tudo
Para casos que exigem múltiplas coleções, use partition:
let numeros = [1, 2, 3, 4, 5, 6];
let (pares, impares): (Vec<_>, Vec<_>) = numeros
.iter()
.partition(|&&x| x % 2 == 0);
8. Boas Práticas e Armadilhas Comuns
Evite múltiplas coletas desnecessárias:
// Ruim: coleta duas vezes
let temp: Vec<_> = dados.iter().map(f).collect();
let resultado: Vec<_> = temp.iter().filter(g).collect();
// Bom: pipeline único
let resultado: Vec<_> = dados.iter().map(f).filter(g).collect();
Cuidado com chain e flatten — eles mantêm a lazy evaluation, mas podem levar a iteradores complexos que são difíceis de depurar:
let iter1 = 0..3;
let iter2 = 10..13;
let combinado = iter1.chain(iter2).map(|x| x * 2);
// Ainda lazy! Só executa quando consumido.
Use inspect para depuração sem perder a preguiça:
(0..10)
.inspect(|x| eprintln!("Antes do filtro: {}", x))
.filter(|x| x % 2 == 0)
.inspect(|x| eprintln!("Depois do filtro: {}", x))
.take(3)
.for_each(|x| println!("Final: {}", x));
A avaliação lazy é um dos pilares do design de Rust. Dominá-la permite escrever código eficiente, expressivo e seguro, aproveitando ao máximo o sistema de tipos e o modelo de ownership.
Referências
- The Rust Programming Language - Iterator Trait — Capítulo oficial do livro sobre iteradores, incluindo explicações sobre lazy vs eager
- Rust by Example - Iterators — Exemplos práticos de uso de iteradores com foco em lazy evaluation
- Rust Reference - Iterator adapters — Documentação completa do módulo
std::itercom todos os adaptadores e consumidores - Lazy vs Eager Evaluation in Rust Iterators - Medium — Artigo técnico detalhando as diferenças de performance entre lazy e eager (link ilustrativo)
- Rust Iterator Cheat Sheet — Referência visual rápida sobre adaptadores lazy e consumidores eager em Rust