Rust

Iteradores e Closures — O Estilo Funcional de Rust Já leu

10 min de leitura

Iteradores e Closures — O Estilo Funcional de Rust
  Nos artigos anteriores usamos for para percorrer coleções e vimos brevemente alguns métodos como .map() e .filter(). Chegou a

 

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, &nota)| (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

Comentários

Mais em Rust

Controle de Fluxo — if, loop, while e for como você nunca viu antes
Controle de Fluxo — if, loop, while e for como você nunca viu antes

No artigo anterior, aprendemos que Rust distingue statements de expressions,...

Construindo uma API REST com Axum
Construindo uma API REST com Axum

Chegamos ao primeiro artigo verdadeiramente orientado a projeto. Até aqui con...

Autenticação JWT — Protegendo sua API com Tokens
Autenticação JWT — Protegendo sua API com Tokens

Nos dois artigos anteriores construímos uma API REST com persistência real. M...