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