Nos artigos anteriores usamos for para percorrer coleções e vimos brevemente alguns métodos como .map() e .filter(). Chegou a hora de entender o que está por baixo dessas abstrações — e por que elas são tão poderosas em Rust.
Iteradores e closures formam juntos o núcleo do estilo funcional de Rust. São abstrações de custo zero — o compilador as otimiza tão bem que o código gerado é equivalente a loops escritos manualmente em C. Você ganha expressividade sem pagar nada em desempenho.
Closures — funções anônimas que capturam o ambiente
Uma closure é uma função anônima que pode capturar variáveis do escopo onde foi definida. A sintaxe usa barras verticais para delimitar os parâmetros:
fn main() {
// Closure simples
let somar = |a, b| a + b;
println!("{}", somar(3, 4)); // 7
// Closure com bloco
let saudacao = |nome: &str| {
let mensagem = format!("Olá, {nome}!");
mensagem
};
println!("{}", saudacao("Ana"));
// Closure que captura o ambiente
let fator = 3;
let multiplicar = |x| x * fator; // captura fator do escopo externo
println!("{}", multiplicar(5)); // 15
}
A diferença fundamental entre closures e funções regulares é a captura de ambiente. Uma closure pode usar variáveis do escopo onde foi criada — sem precisar que sejam passadas como parâmetros.
Captura por referência, por valor
Rust captura variáveis da forma mais econômica possível. Por padrão, tenta capturar por referência. Se a closure precisar de posse, use move:
fn main() {
let texto = String::from("mundo");
// Captura por referência — texto ainda existe aqui
let imprimir = || println!("Olá, {texto}");
imprimir();
println!("texto ainda existe: {texto}");
// Captura por valor com move — necessário quando a closure
// precisa sobreviver ao escopo original
let texto2 = String::from("Rust");
let imprimir2 = move || println!("Olá, {texto2}");
// texto2 foi movido para dentro da closure
imprimir2();
}
O move é especialmente importante em programação concorrente, quando closures são enviadas para outras threads. Veremos isso no artigo sobre concorrência.
Closures como parâmetros
Closures podem ser passadas como argumentos para funções. O tipo de uma closure é descrito por traits: Fn, FnMut ou FnOnce:
fn aplicar<F: Fn(i32) -> i32>(f: F, valor: i32) -> i32 {
f(valor)
}
fn aplicar_duas_vezes<F: Fn(i32) -> i32>(f: F, valor: i32) -> i32 {
f(f(valor))
}
fn main() {
let dobrar = |x| x * 2;
let somar_dez = |x| x + 10;
println!("{}", aplicar(dobrar, 5)); // 10
println!("{}", aplicar_duas_vezes(dobrar, 3)); // 12
println!("{}", aplicar_duas_vezes(somar_dez, 5)); // 25
}
Os três traits de closure diferem no que fazem com o ambiente capturado:
Fn — lê o ambiente por referência. Pode ser chamada múltiplas vezes. FnMut — modifica o ambiente por referência mutável. Pode ser chamada múltiplas vezes. FnOnce — consome o ambiente. Pode ser chamada apenas uma vez.
Toda closure que implementa Fn também implementa FnMut e FnOnce — é uma hierarquia. Use Fn como constraint padrão — o compilador avisa se precisar de algo mais permissivo.
Iteradores — processamento preguiçoso de sequências
Um iterador é qualquer tipo que implementa o trait Iterator, que exige apenas um método: next(), que retorna Option<Item> — Some(item) enquanto houver elementos, None quando a sequência terminar.
fn main() {
let v = vec![1, 2, 3];
let mut iter = v.iter(); // cria o iterador
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
}
O laço for é açúcar sintático para esse processo — ele chama next() repetidamente até receber None.
Iteradores são preguiçosos
Iteradores não fazem nada até serem consumidos. Você pode encadear transformações sem que nenhuma delas seja executada até o momento do consumo:
fn main() {
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Nada é executado ainda — apenas uma cadeia de adaptadores
let cadeia = v.iter()
.filter(|&&x| x % 2 == 0) // pares
.map(|&x| x * x); // ao quadrado
// Agora sim — collect consome o iterador
let resultado: Vec<i32> = cadeia.collect();
println!("{:?}", resultado); // [4, 16, 36, 64, 100]
}
Essa preguiça tem implicações de desempenho: o compilador pode otimizar toda a cadeia em um único loop, sem alocar memória intermediária.
Os adaptadores mais importantes
Adaptadores transformam um iterador em outro. São o coração do estilo funcional em Rust.
map — transformar elementos
fn main() {
let nomes = vec!["ana", "carlos", "maria"];
let maiusculos: Vec<String> = nomes.iter()
.map(|n| n.to_uppercase())
.collect();
println!("{:?}", maiusculos);
// ["ANA", "CARLOS", "MARIA"]
}
filter — selecionar elementos
fn main() {
let numeros = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let pares: Vec<&i32> = numeros.iter()
.filter(|&&x| x % 2 == 0)
.collect();
println!("{:?}", pares); // [2, 4, 6, 8, 10]
}
map + filter encadeados
fn main() {
let palavras = vec!["rust", "go", "python", "java", "c", "kotlin"];
let longas_maiusculas: Vec<String> = palavras.iter()
.filter(|p| p.len() > 3)
.map(|p| p.to_uppercase())
.collect();
println!("{:?}", longas_maiusculas);
// ["PYTHON", "JAVA", "KOTLIN"]
}
enumerate — índice junto com o valor
fn main() {
let linguagens = vec!["Rust", "Go", "Python", "Kotlin"];
for (i, lang) in linguagens.iter().enumerate() {
println!("{}. {lang}", i + 1);
}
}
zip — combinar dois iteradores
fn main() {
let nomes = vec!["Ana", "Carlos", "Maria"];
let notas = vec![9.5, 7.2, 8.8];
let combinado: Vec<(&str, f64)> = nomes.iter()
.zip(notas.iter())
.map(|(&n, ¬a)| (n, nota))
.collect();
for (nome, nota) in &combinado {
println!("{nome}: {nota:.1}");
}
}
flat_map — mapear e achatar
fn main() {
let frases = vec!["olá mundo", "rust é incrível"];
let palavras: Vec<&str> = frases.iter()
.flat_map(|frase| frase.split_whitespace())
.collect();
println!("{:?}", palavras);
// ["olá", "mundo", "rust", "é", "incrível"]
}
take e skip — fatiar sequências
fn main() {
let v: Vec<i32> = (1..=10).collect();
let primeiros_tres: Vec<&i32> = v.iter().take(3).collect();
let pulando_quatro: Vec<&i32> = v.iter().skip(4).collect();
println!("Take 3: {:?}", primeiros_tres); // [1, 2, 3]
println!("Skip 4: {:?}", pulando_quatro); // [5, 6, 7, 8, 9, 10]
}
chain — concatenar iteradores
fn main() {
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let unidos: Vec<&i32> = a.iter().chain(b.iter()).collect();
println!("{:?}", unidos); // [1, 2, 3, 4, 5, 6]
}
Os consumidores mais importantes
Consumidores encerram a cadeia e produzem um valor final.
collect — materializar em coleção
Já vimos bastante. Pode coletar em Vec, HashMap, HashSet, String e outros:
use std::collections::{HashMap, HashSet};
fn main() {
let pares = vec![("um", 1), ("dois", 2), ("três", 3)];
// Em HashMap
let mapa: HashMap<&str, i32> = pares.into_iter().collect();
println!("{:?}", mapa);
// Em HashSet
let v = vec![1, 2, 2, 3, 3, 3];
let unicos: HashSet<i32> = v.into_iter().collect();
println!("{:?}", unicos); // {1, 2, 3}
}
fold — reduzir a um valor
fold acumula um resultado percorrendo todos os elementos:
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
let soma = numeros.iter().fold(0, |acc, &x| acc + x);
let produto = numeros.iter().fold(1, |acc, &x| acc * x);
println!("Soma: {soma}"); // 15
println!("Produto: {produto}"); // 120
}
sum e product — atalhos para fold comum
fn main() {
let numeros = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let soma: f64 = numeros.iter().sum();
let produto: f64 = numeros.iter().product();
println!("Soma: {soma}"); // 15
println!("Produto: {produto}"); // 120
}
any e all — verificações booleanas
fn main() {
let notas = vec![7.5, 8.0, 6.5, 9.0, 5.5];
let algum_reprovado = notas.iter().any(|&n| n < 6.0);
let todos_aprovados = notas.iter().all(|&n| n >= 6.0);
println!("Algum reprovado? {algum_reprovado}"); // true
println!("Todos aprovados? {todos_aprovados}"); // false
}
find e position — busca
fn main() {
let frutas = vec!["maçã", "banana", "laranja", "uva"];
let tem_a = frutas.iter().find(|&&f| f.contains('a'));
println!("{:?}", tem_a); // Some("maçã")
let pos = frutas.iter().position(|&f| f == "laranja");
println!("{:?}", pos); // Some(2)
}
min, max, min_by, max_by
fn main() {
let numeros = vec![3, 1, 4, 1, 5, 9, 2, 6];
println!("Min: {:?}", numeros.iter().min()); // Some(1)
println!("Max: {:?}", numeros.iter().max()); // Some(9)
let decimais = vec![3.1f64, 1.4, 5.9, 2.6];
let menor = decimais.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap());
println!("Menor decimal: {:?}", menor); // Some(1.4)
}
Criando seus próprios iteradores
Qualquer tipo que implemente Iterator pode ser usado em toda a infraestrutura de iteradores. Veja um exemplo simples — um iterador de Fibonacci:
struct Fibonacci {
atual: u64,
proximo: u64,
}
impl Fibonacci {
fn novo() -> Fibonacci {
Fibonacci { atual: 0, proximo: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let novo_proximo = self.atual + self.proximo;
self.atual = self.proximo;
self.proximo = novo_proximo;
Some(self.atual) // infinito — nunca retorna None
}
}
fn main() {
let fib: Vec<u64> = Fibonacci::novo()
.take(10)
.collect();
println!("{:?}", fib);
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
// Primeiro número de Fibonacci maior que 1000
let grande = Fibonacci::novo()
.find(|&n| n > 1000);
println!("{:?}", grande); // Some(1597)
}
Implementando apenas next(), ganhamos automaticamente acesso a todos os adaptadores e consumidores da biblioteca padrão: take, filter, map, fold, tudo funciona.
Um programa completo: análise de vendas
#[derive(Debug)]
struct Venda {
produto: String,
categoria: String,
valor: f64,
quantidade: u32,
}
impl Venda {
fn nova(produto: &str, categoria: &str, valor: f64, quantidade: u32) -> Venda {
Venda {
produto: produto.to_string(),
categoria: categoria.to_string(),
valor,
quantidade,
}
}
fn total(&self) -> f64 {
self.valor * self.quantidade as f64
}
}
fn main() {
let vendas = vec![
Venda::nova("Notebook", "Eletrônicos", 3500.0, 2),
Venda::nova("Teclado", "Eletrônicos", 250.0, 5),
Venda::nova("Cadeira", "Móveis", 890.0, 3),
Venda::nova("Monitor", "Eletrônicos", 1200.0, 4),
Venda::nova("Mesa", "Móveis", 1500.0, 1),
Venda::nova("Headset", "Eletrônicos", 350.0, 6),
Venda::nova("Estante", "Móveis", 600.0, 2),
];
// Total geral
let total_geral: f64 = vendas.iter().map(|v| v.total()).sum();
println!("Total geral: R$ {total_geral:.2}");
// Venda de maior valor unitário
let maior = vendas.iter()
.max_by(|a, b| a.valor.partial_cmp(&b.valor).unwrap());
if let Some(v) = maior {
println!("Produto mais caro: {} (R$ {:.2})", v.produto, v.valor);
}
// Total por categoria
let categorias = ["Eletrônicos", "Móveis"];
println!("\n── Por Categoria ──");
for cat in &categorias {
let total_cat: f64 = vendas.iter()
.filter(|v| v.categoria == *cat)
.map(|v| v.total())
.sum();
let percentual = total_cat / total_geral * 100.0;
println!("{cat}: R$ {total_cat:.2} ({percentual:.1}%)");
}
// Produtos com total acima de R$ 2000
println!("\n── Destaques (total > R$ 2000) ──");
let mut destaques: Vec<&Venda> = vendas.iter()
.filter(|v| v.total() > 2000.0)
.collect();
destaques.sort_by(|a, b| b.total().partial_cmp(&a.total()).unwrap());
for v in destaques {
println!("{}: R$ {:.2}", v.produto, v.total());
}
}
Saída:
Total geral: R$ 20830.00
Produto mais caro: Notebook (R$ 3500.00)
── Por Categoria ──
Eletrônicos: R$ 14550.00 (69.9%)
Móveis: R$ 6280.00 (30.1%)
── Destaques (total > R$ 2000) ──
Notebook: R$ 7000.00
Monitor: R$ 4800.00
Cadeira: R$ 2670.00
Headset: R$ 2100.00
Iteradores vs loops — quando usar cada um
Uma dúvida legítima: quando usar iteradores com .map().filter().collect() e quando usar um for simples?
Use iteradores quando estiver transformando dados de forma declarativa — filtrando, mapeando, reduzindo. O código comunica a intenção, não os passos. É mais fácil de ler e raramente mais lento que um loop manual.
Use for loops quando a lógica é complexa, envolve múltiplas coleções de forma não linear, ou quando o estado interno do loop é difícil de expressar com adaptadores. Não force iteradores onde um loop é mais claro.
Rust não é uma linguagem puramente funcional — loops e iteradores coexistem, e a escolha deve servir à legibilidade.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 13 — Closures and Iterators — https://doc.rust-lang.org/book/ch13-00-functional-features.html
- Rust by Example — Closures — https://doc.rust-lang.org/rust-by-example/fn/closures.html
- Rust by Example — Iterators — https://doc.rust-lang.org/rust-by-example/trait/iter.html
- Rust Standard Library — Iterator trait — documentação completa com todos os métodos — https://doc.rust-lang.org/std/iter/trait.Iterator.html
- "Iterators in Rust" — Amos (fasterthanli.me) — artigo aprofundado sobre o sistema de iteradores — https://fasterthanli.me/articles/working-with-strings-in-rust
- Rustlings, seção
iterators— https://github.com/rust-lang/rustlings