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. 13 — Functional 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
rayoncrate — paralelismo com iteradores — https://docs.rs/rayonitertoolscrate — adaptadores extras para iteradores — https://docs.rs/itertools