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

Traits — Definindo Comportamento Compartilhado
Traits — Definindo Comportamento Compartilhado

&nbsp; Nos artigos anteriores criamos structs e enums para modelar dados. Ma...

Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu
Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu

&nbsp; Se structs s&atilde;o a forma de Rust agrupar dados relacionados,&nbs...

Por que Rust existe e o que ele quer de você
Por que Rust existe e o que ele quer de você

Toda linguagem de programa&ccedil;&atilde;o nasce de uma frustra&ccedil;&atil...