Rust

Closures Avançadas e Programação Funcional — Indo Além do map e filter Já leu

12 min de leitura

Closures Avançadas e Programação Funcional — Indo Além do map e filter
  Em um artigo anterior, introduzimos closures e os adaptadores mais comuns de iteradores. Usamos map, filter, fold e collect — ferramentas poderosa

 

Em um artigo anterior, introduzimos closures e os adaptadores mais comuns de iteradores. Usamos map, filter, fold e collect — ferramentas poderosas que já transformam a forma de escrever código. Mas closures em Rust têm profundidade maior do que o uso básico sugere. Neste artigo vamos explorar closures como valores de primeira classe, padrões funcionais avançados, e como compor comportamentos de formas que seriam verbosas ou impossíveis em código imperativo puro.


Closures armazenadas em structs

Closures podem ser armazenadas como campos de structs — o que permite criar objetos que encapsulam comportamento configurável:

struct Processador<F>
where
    F: Fn(f64) -> f64,
{
    nome: String,
    transformacao: F,
}

impl<F: Fn(f64) -> f64> Processador<F> {
    fn novo(nome: &str, transformacao: F) -> Self {
        Processador {
            nome: nome.to_string(),
            transformacao,
        }
    }

    fn processar(&self, valor: f64) -> f64 {
        (self.transformacao)(valor)
    }

    fn processar_lista(&self, valores: &[f64]) -> Vec<f64> {
        valores.iter().map(|&v| self.processar(v)).collect()
    }
}

fn main() {
    let duplicador = Processador::novo("duplicar", |x| x * 2.0);
    let quadrador  = Processador::novo("quadrado", |x| x * x);
    let normalizador = Processador::novo("normalizar", |x| {
        (x - 0.0) / (100.0 - 0.0) // escala de 0-100 para 0-1
    });

    let dados = vec![1.0, 5.0, 10.0, 25.0, 50.0];

    println!("Original:    {:?}", dados);
    println!("Duplicado:   {:?}", duplicador.processar_lista(&dados));
    println!("Quadrado:    {:?}", quadrador.processar_lista(&dados));
    println!("Normalizado: {:?}", normalizador.processar_lista(&dados));
}

Saída:

Original:    [1.0, 5.0, 10.0, 25.0, 50.0]
Duplicado:   [2.0, 10.0, 20.0, 50.0, 100.0]
Quadrado:    [1.0, 25.0, 100.0, 625.0, 2500.0]
Normalizado: [0.01, 0.05, 0.1, 0.25, 0.5]

Cache com memoização

Um padrão clássico de programação funcional é a memoização — armazenar o resultado de chamadas anteriores para evitar recalcular. Closures tornam isso elegante:

use std::collections::HashMap;

struct Memoizado<F, A, R>
where
    F: Fn(A) -> R,
    A: Eq + std::hash::Hash + Clone,
    R: Clone,
{
    funcao: F,
    cache: HashMap<A, R>,
}

impl<F, A, R> Memoizado<F, A, R>
where
    F: Fn(A) -> R,
    A: Eq + std::hash::Hash + Clone,
    R: Clone,
{
    fn novo(funcao: F) -> Self {
        Memoizado {
            funcao,
            cache: HashMap::new(),
        }
    }

    fn calcular(&mut self, argumento: A) -> R {
        if let Some(resultado) = self.cache.get(&argumento) {
            println!("  [cache hit] {:?}", &argumento);
            return resultado.clone();
        }

        println!("  [calculando] {:?}", &argumento);
        let resultado = (self.funcao)(argumento.clone());
        self.cache.insert(argumento, resultado.clone());
        resultado
    }
}

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    let mut memo = Memoizado::novo(fibonacci);

    println!("Calculando fibonacci:");
    for &n in &[10, 15, 10, 20, 15, 25] {
        let resultado = memo.calcular(n);
        println!("  fib({n}) = {resultado}");
    }
}

Saída:

Calculando fibonacci:
  [calculando] 10
  fib(10) = 55
  [calculando] 15
  fib(15) = 610
  [cache hit] 10
  fib(10) = 55
  [calculando] 20
  fib(20) = 6765
  [cache hit] 15
  fib(15) = 610
  [calculando] 25
  fib(25) = 75025

Composição de funções

Em programação funcional, compor funções — aplicar uma após a outra — é um padrão fundamental. Em Rust, podemos criar um combinador genérico:

fn compor<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    move |x| g(f(x))
}

fn main() {
    let dobrar    = |x: i32| x * 2;
    let somar_um  = |x: i32| x + 1;
    let quadrado  = |x: i32| x * x;

    // Composição: dobrar, depois somar_um
    let dobrar_e_somar = compor(dobrar, somar_um);
    println!("dobrar_e_somar(5) = {}", dobrar_e_somar(5)); // 11

    // Composição: dobrar, somar_um, quadrado
    let pipeline = compor(compor(dobrar, somar_um), quadrado);
    println!("pipeline(5) = {}", pipeline(5)); // 121

    // Composição com strings
    let preparar_texto = compor(
        |s: String| s.trim().to_string(),
        |s: String| s.to_uppercase(),
    );
    println!("{}", preparar_texto(String::from("  olá mundo  ")));
    // OLÁ MUNDO
}

Closures que retornam closures

Funções podem retornar closures — criando fábricas de comportamento:

fn criar_multiplicador(fator: f64) -> impl Fn(f64) -> f64 {
    move |x| x * fator
}

fn criar_somador(incremento: f64) -> impl Fn(f64) -> f64 {
    move |x| x + incremento
}

fn criar_validador(min: f64, max: f64) -> impl Fn(f64) -> bool {
    move |x| x >= min && x <= max
}

fn criar_formatador(prefixo: &str, casas: usize) -> impl Fn(f64) -> String + '_ {
    move |x| format!("{prefixo}{x:.casas$}")
}

fn main() {
    let triplicar  = criar_multiplicador(3.0);
    let mais_dez   = criar_somador(10.0);
    let percentual = criar_validador(0.0, 100.0);
    let formatar   = criar_formatador("R$ ", 2);

    println!("{}", triplicar(7.0));           // 21
    println!("{}", mais_dez(5.0));            // 15
    println!("{}", percentual(75.0));         // true
    println!("{}", percentual(150.0));        // false
    println!("{}", formatar(1234.5));         // R$ 1234.50

    // Aplicando em sequência
    let valores = vec![10.0, 20.0, 30.0, 40.0, 50.0];
    let resultado: Vec<String> = valores.iter()
        .map(|&v| triplicar(v))
        .map(|v| mais_dez(v))
        .filter(|&v| percentual(v))
        .map(|v| formatar(v))
        .collect();

    println!("{:?}", resultado);
}

Saída:

21
15
true
false
R$ 1234.50
["R$ 40.00", "R$ 70.00"]

O padrão Builder com closures

Closures permitem criar APIs fluentes e configuráveis — o padrão Builder em sua forma mais expressiva:

struct Relatorio {
    titulo: String,
    dados: Vec<f64>,
    filtro: Box<dyn Fn(f64) -> bool>,
    transformacao: Box<dyn Fn(f64) -> f64>,
    formatador: Box<dyn Fn(f64) -> String>,
}

impl Relatorio {
    fn novo(titulo: &str, dados: Vec<f64>) -> Self {
        Relatorio {
            titulo: titulo.to_string(),
            dados,
            filtro: Box::new(|_| true),
            transformacao: Box::new(|x| x),
            formatador: Box::new(|x| format!("{x:.2}")),
        }
    }

    fn com_filtro(mut self, f: impl Fn(f64) -> bool + 'static) -> Self {
        self.filtro = Box::new(f);
        self
    }

    fn com_transformacao(mut self, f: impl Fn(f64) -> f64 + 'static) -> Self {
        self.transformacao = Box::new(f);
        self
    }

    fn com_formatador(mut self, f: impl Fn(f64) -> String + 'static) -> Self {
        self.formatador = Box::new(f);
        self
    }

    fn gerar(&self) {
        println!("══ {} ══", self.titulo);

        let processados: Vec<f64> = self.dados.iter()
            .filter(|&&v| (self.filtro)(v))
            .map(|&v| (self.transformacao)(v))
            .collect();

        if processados.is_empty() {
            println!("  Nenhum dado após filtragem.");
            return;
        }

        for valor in &processados {
            println!("  {}", (self.formatador)(*valor));
        }

        let soma: f64 = processados.iter().sum();
        let media = soma / processados.len() as f64;
        let max = processados.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        let min = processados.iter().cloned().fold(f64::INFINITY, f64::min);

        println!("──────────────────────");
        println!("  Total : {}", (self.formatador)(soma));
        println!("  Média : {}", (self.formatador)(media));
        println!("  Máx   : {}", (self.formatador)(max));
        println!("  Mín   : {}", (self.formatador)(min));
    }
}

fn main() {
    let vendas = vec![150.0, 320.0, 80.0, 450.0, 210.0, 55.0, 380.0, 90.0];

    // Relatório 1: todas as vendas formatadas em reais
    Relatorio::novo("Vendas do Mês", vendas.clone())
        .com_formatador(|v| format!("R$ {v:>8.2}"))
        .gerar();

    println!();

    // Relatório 2: apenas vendas acima de 100, com 10% de bônus
    Relatorio::novo("Vendas Qualificadas (+10% bônus)", vendas.clone())
        .com_filtro(|v| v > 100.0)
        .com_transformacao(|v| v * 1.10)
        .com_formatador(|v| format!("R$ {v:>8.2}"))
        .gerar();

    println!();

    // Relatório 3: distribuição percentual
    let total: f64 = vendas.iter().sum();
    Relatorio::novo("Distribuição Percentual", vendas.clone())
        .com_transformacao(move |v| v / total * 100.0)
        .com_formatador(|v| format!("{v:>6.1}%"))
        .gerar();
}

Saída:

══ Vendas do Mês ══
  R$   150.00
  R$   320.00
  R$    80.00
  R$   450.00
  R$   210.00
  R$    55.00
  R$   380.00
  R$    90.00
──────────────────────
  Total : R$  1735.00
  Média : R$   216.88
  Máx   : R$   450.00
  Mín   : R$    55.00

══ Vendas Qualificadas (+10% bônus) ══
  R$   165.00
  R$   352.00
  R$   495.00
  R$   231.00
  R$   418.00
──────────────────────
  Total : R$  1661.00
  Média : R$   332.20
  Máx   : R$   495.00
  Mín   : R$   165.00

══ Distribuição Percentual ══
   8.6%
  18.4%
   4.6%
  25.9%
  12.1%
   3.2%
  21.9%
   5.2%
──────────────────────
  Total :  100.0%
  Média :   12.5%
  Máx   :   25.9%
  Mín   :    3.2%

Iteradores avançados

Além dos adaptadores básicos, a biblioteca padrão oferece combinadores mais sofisticados:

scan — acumulador com estado visível

Como fold, mas emite cada valor intermediário:

fn main() {
    let transacoes = vec![100.0, -30.0, 50.0, -20.0, 80.0, -10.0];

    let saldos: Vec<f64> = transacoes.iter()
        .scan(0.0, |saldo, &transacao| {
            *saldo += transacao;
            Some(*saldo)
        })
        .collect();

    println!("Transações: {:?}", transacoes);
    println!("Saldos:     {:?}", saldos);
    // Saldos: [100.0, 70.0, 120.0, 100.0, 180.0, 170.0]
}

window com zip — análise de pares consecutivos

fn main() {
    let precos = vec![100.0, 105.0, 98.0, 112.0, 108.0, 115.0];

    let variacoes: Vec<f64> = precos.windows(2)
        .map(|w| (w[1] - w[0]) / w[0] * 100.0)
        .collect();

    println!("Preços:    {:?}", precos);
    println!("Variações (%):");
    for (i, var) in variacoes.iter().enumerate() {
        let sinal = if *var >= 0.0 { "▲" } else { "▼" };
        println!("  Dia {}->{}: {sinal} {:.2}%",
            i + 1, i + 2, var.abs());
    }

    let media_variacao: f64 = variacoes.iter().sum::<f64>()
        / variacoes.len() as f64;
    println!("Variação média: {:.2}%", media_variacao);
}

Saída:

Preços:    [100.0, 105.0, 98.0, 112.0, 108.0, 115.0]
Variações (%):
  Dia 1->2: ▲ 5.00%
  Dia 2->3: ▼ 6.67%
  Dia 3->4: ▲ 14.29%
  Dia 4->5: ▼ 3.57%
  Dia 5->6: ▲ 6.48%
Variação média: 3.11%

chunks — processar em lotes

fn main() {
    let dados = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    println!("Processando em lotes de 3:");
    for (i, lote) in dados.chunks(3).enumerate() {
        let soma: i32 = lote.iter().sum();
        println!("  Lote {}: {:?} → soma={soma}", i + 1, lote);
    }
}

Saída:

Processando em lotes de 3:
  Lote 1: [1, 2, 3] → soma=6
  Lote 2: [4, 5, 6] → soma=15
  Lote 3: [7, 8, 9] → soma=24
  Lote 4: [10] → soma=10

partition — dividir em dois grupos

fn main() {
    let numeros: Vec<i32> = (1..=20).collect();

    let (pares, impares): (Vec<i32>, Vec<i32>) = numeros
        .iter()
        .partition(|&&n| n % 2 == 0);

    println!("Pares:   {:?}", pares);
    println!("Ímpares: {:?}", impares);

    // Particionando structs
    #[derive(Debug)]
    struct Produto { nome: String, estoque: u32 }

    let produtos = vec![
        Produto { nome: "A".into(), estoque: 0  },
        Produto { nome: "B".into(), estoque: 10 },
        Produto { nome: "C".into(), estoque: 0  },
        Produto { nome: "D".into(), estoque: 5  },
    ];

    let (disponiveis, esgotados): (Vec<_>, Vec<_>) = produtos
        .into_iter()
        .partition(|p| p.estoque > 0);

    println!("Disponíveis: {:?}", disponiveis.iter().map(|p| &p.nome).collect::<Vec<_>>());
    println!("Esgotados:   {:?}", esgotados.iter().map(|p| &p.nome).collect::<Vec<_>>());
}

Um programa completo: pipeline de processamento de dados

Vamos combinar tudo em um sistema de processamento de dados com estágios configuráveis:

type Transformador = Box<dyn Fn(f64) -> f64>;
type Filtro = Box<dyn Fn(f64) -> bool>;

struct Pipeline {
    nome: String,
    filtros: Vec<Filtro>,
    transformacoes: Vec<Transformador>,
}

impl Pipeline {
    fn novo(nome: &str) -> Self {
        Pipeline {
            nome: nome.to_string(),
            filtros: Vec::new(),
            transformacoes: Vec::new(),
        }
    }

    fn filtrar(mut self, f: impl Fn(f64) -> bool + 'static) -> Self {
        self.filtros.push(Box::new(f));
        self
    }

    fn transformar(mut self, f: impl Fn(f64) -> f64 + 'static) -> Self {
        self.transformacoes.push(Box::new(f));
        self
    }

    fn executar(&self, dados: &[f64]) -> Vec<f64> {
        dados.iter()
            .cloned()
            .filter(|&v| self.filtros.iter().all(|f| f(v)))
            .map(|v| self.transformacoes.iter().fold(v, |acc, t| t(acc)))
            .collect()
    }

    fn relatorio(&self, dados: &[f64]) {
        let resultado = self.executar(dados);

        println!("══ Pipeline: {} ══", self.nome);
        println!("  Entrada : {} valores", dados.len());
        println!("  Saída   : {} valores", resultado.len());

        if !resultado.is_empty() {
            let soma: f64 = resultado.iter().sum();
            let media = soma / resultado.len() as f64;
            println!("  Soma    : {:.2}", soma);
            println!("  Média   : {:.2}", media);
            println!("  Valores : {:?}", resultado
                .iter()
                .map(|v| format!("{v:.1}"))
                .collect::<Vec<_>>());
        }
    }
}

fn main() {
    let dados_brutos = vec![
        -5.0, 0.0, 3.5, 12.0, 7.8, -2.1, 45.0, 23.4,
        8.9, 100.0, 55.5, 0.5, 33.0, -10.0, 67.8,
    ];

    println!("Dados brutos: {:?}
", dados_brutos);

    // Pipeline 1: positivos normalizados
    Pipeline::novo("Positivos Normalizados")
        .filtrar(|v| v > 0.0)
        .transformar(|v| v / 100.0)
        .relatorio(&dados_brutos);

    println!();

    // Pipeline 2: intervalo médio com imposto
    Pipeline::novo("Intervalo Médio + Imposto 15%")
        .filtrar(|v| v >= 5.0 && v <= 50.0)
        .transformar(|v| v * 1.15)
        .transformar(|v| (v * 100.0).round() / 100.0)
        .relatorio(&dados_brutos);

    println!();

    // Pipeline 3: análise estatística dos não-negativos
    let nao_negativos: Vec<f64> = dados_brutos.iter()
        .cloned()
        .filter(|&v| v >= 0.0)
        .collect();

    let max = nao_negativos.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

    Pipeline::novo("Percentual do Máximo")
        .filtrar(|v| v >= 0.0)
        .transformar(move |v| v / max * 100.0)
        .transformar(|v| (v * 10.0).round() / 10.0)
        .relatorio(&dados_brutos);
}

Saída:

Dados brutos: [-5.0, 0.0, 3.5, 12.0, ...]

══ Pipeline: Positivos Normalizados ══
  Entrada : 15 valores
  Saída   : 10 valores
  Soma    : 3.57
  Média   : 0.36
  Valores : ["0.0", "0.0", "0.1", "0.1", ...]

══ Pipeline: Intervalo Médio + Imposto 15% ══
  Entrada : 15 valores
  Saída   : 5 valores
  Soma    : 130.65
  Média   : 26.13
  Valores : ["4.02", "13.8", "8.97", "10.24", "26.45", ...]

══ Pipeline: Percentual do Máximo ══
  Entrada : 15 valores
  Saída   : 11 valores
  Soma    : 353.5
  Média   : 32.1
  Valores : ["0.0", "3.5", "12.0", "7.8", ...]

A mentalidade funcional em Rust

Rust não é uma linguagem puramente funcional — mas abraça fortemente conceitos funcionais onde eles trazem clareza e segurança. A imutabilidade por padrão, os iteradores sem efeitos colaterais, e a composição de closures são todos reflexos dessa influência.

O estilo funcional em Rust não é apenas estético. Código escrito com iteradores e closures é frequentemente mais fácil de paralelizar — e a crate rayon permite converter iteradores sequenciais em paralelos com uma mudança de uma linha, que exploraremos no artigo sobre concorrência.

A regra prática: quando você se pegar escrevendo um loop com uma variável acumuladora, considere se um fold, scan ou reduce expressaria melhor a intenção. Nem sempre a resposta é sim — mas vale a reflexão.


Fontes e leituras recomendadas

  • The Rust Programming Language, Cap. 13Functional Language Features: Iterators and Closures — https://doc.rust-lang.org/book/ch13-00-functional-features.html
  • Rust by Example — Higher Order Functions — https://doc.rust-lang.org/rust-by-example/fn/hof.html
  • Rust Standard Library — Iterator — lista completa de todos os adaptadores — https://doc.rust-lang.org/std/iter/trait.Iterator.html
  • "Functional Programming Jargon in Rust" — tradução de conceitos funcionais para Rust — https://functional.works-hub.com/learn/functional-programming-jargon-in-rust
  • rayon crate — paralelismo com iteradores — https://docs.rs/rayon
  • itertools crate — adaptadores extras para iteradores — https://docs.rs/itertools
Comentários

Mais em Rust

Variáveis, Tipos e a Arte da Imutabilidade
Variáveis, Tipos e a Arte da Imutabilidade

Em um artigo anterior, instalamos o Rust e entendemos por que ele existe. Hoj...

Generics — Código que Funciona para Qualquer Tipo
Generics — Código que Funciona para Qualquer Tipo

&nbsp; No artigo anterior aprendemos que traits definem&nbsp;o que um tipo p...

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,...