Iterator e closure em Rust

1. Visão geral

Closure e iterator são duas ferramentas que ajudam você a processar dados de forma funcional em Rust enquanto mantém o desempenho equivalente a um loop escrito manualmente. Este artigo explica como closure captura variáveis do escopo ao redor e os três traits Fn, FnMut, FnOnce. Depois passa pelo trait Iterator, seus adaptadores comuns e como escrever seu próprio iterator. Após ler, você conseguirá escrever pipelines como filter → map → collect com confiança. Você também entenderá por que o compilador às vezes rejeita seu código.

Todos os exemplos usam Rust 1.80+ (edição 2021), apenas com a biblioteca padrão.

2. Closure básico

2.1. Sintaxe

Closure é uma função anônima que pode “enxergar” variáveis do escopo ao redor:

fn main() {
    let discount = 10; // desconto em percentual
    let apply_discount = |price: u32| price - price * discount / 100;

    println!("{}", apply_discount(250));
    println!("{}", apply_discount(1000));
}
225
900

Os parâmetros ficam entre as duas barras |...|, e o corpo da closure vem logo depois. A closure usa a variável discount mesmo sem ela ser passada como parâmetro. Uma função fn comum não conseguiria fazer isso.

Os tipos de parâmetro e o tipo de retorno geralmente são inferidos automaticamente. Você pode escrever |price| ... se o compilador tiver informação suficiente. No entanto, o tipo é inferido apenas uma vez. Se você chamar a closure com u32 pela primeira vez e depois com i64, terá erro de compilação.

2.2. Três formas de capturar variáveis

A closure captura variáveis da forma “mais leve” que o corpo da closure precisa. A ordem de prioridade é empréstimo imutável, depois empréstimo mutável, e por fim a transferência de propriedade.

fn main() {
    let mut cart = vec!["caneta"];

    let print_cart = || println!("Carrinho: {:?}", cart); // empréstimo &cart
    print_cart();

    let mut add_item = |item| cart.push(item);        // empréstimo &mut cart
    add_item("caderno");
    add_item("régua");

    println!("No final: {:?}", cart);
}
Carrinho: ["caneta"]
No final: ["caneta", "caderno", "régua"]

Analisando cada closure:

  • print_cart apenas lê cart, então mantém um &cart.
  • add_item chama push, então precisa de &mut cart. A variável closure também deve ser declarada mut, pois chamá-la altera o estado interno.
  • O código compila graças aos non-lexical lifetimes. print_cart não é mais usada após a primeira chamada, então o empréstimo imutável termina antes do empréstimo mutável começar.

Quando você precisa que a closure possua os dados, adicione a palavra-chave move. O caso típico é passar a closure para uma thread diferente:

use std::thread;

fn main() {
    let customer = String::from("Lan");

    let handle = thread::spawn(move || {
        println!("Processando pedido para {customer}");
    });

    handle.join().unwrap();
}
Processando pedido para Lan

Sem move, o compilador reporta erro. A nova thread pode viver mais tempo que main, então uma referência a customer não seria segura. Com move, a String é transferida para dentro da closure. Depois dessa linha, você não pode usar customer em main novamente.

3. Os três traits Fn, FnMut e FnOnce

Cada closure implementa automaticamente um ou mais dos três traits a seguir. Qual trait é implementado depende do que a closure faz com as variáveis capturadas:

Trait O que a closure faz com variáveis capturadas Pode ser chamada quantas vezes
Fn Apenas lê Muitas vezes
FnMut Modifica Muitas vezes
FnOnce Consome (move para fora) Uma vez

Estes três traits têm uma relação de herança: todo Fn é também FnMut, e todo FnMut é também FnOnce. Ao escrever uma função que recebe uma closure, você deve escolher o trait mais permissivo que a função precisa. Assim o chamador tem as mais opções possíveis.

fn run_twice<F: Fn()>(f: F) {
    f();
    f();
}

fn run_twice_mut<F: FnMut()>(mut f: F) {
    f();
    f();
}

fn run_once<F: FnOnce() -> String>(f: F) -> String {
    f()
}

fn main() {
    let name = String::from("Minh");
    run_twice(|| println!("Olá {name}"));

    let mut count = 0;
    run_twice_mut(|| count += 1);
    println!("count = {count}");

    let receipt = run_once(move || name + " - pagamento realizado");
    println!("{receipt}");
}
Olá Minh
Olá Minh
count = 2
Minh - pagamento realizado

As três chamadas correspondem aos três traits:

  • A primeira closure apenas lê name, então é Fn.
  • A segunda closure incrementa count, então é FnMut. Passá-la para run_twice resultaria em erro.
  • A última closure usa name + "...". O operador + em String consome name, então a closure só pode ser chamada uma vez e implementa apenas FnOnce.

3.1. Retornando closure de uma função

Cada closure tem seu próprio tipo, anônimo. Por isso você não consegue escrever o nome desse tipo. Existem duas formas de retornar uma closure:

fn make_discount(percent: u32) -> impl Fn(u32) -> u32 {
    move |price| price - price * percent / 100
}

fn pick_rule(is_vip: bool) -> Box<dyn Fn(u32) -> u32> {
    if is_vip {
        Box::new(|price| price * 80 / 100)
    } else {
        Box::new(|price| price)
    }
}

fn main() {
    let summer_sale = make_discount(15);
    println!("{}", summer_sale(200));

    let rule = pick_rule(true);
    println!("{}", rule(500));
}
170
400
  • impl Fn(...) é usado quando a função sempre retorna um tipo de closure fixo. Essa abordagem não aloca heap e não tem overhead de chamada dinâmica.
  • Box<dyn Fn(...)> é usado quando diferentes ramificações retornam closures diferentes. As duas closures em pick_rule têm tipos diferentes, então devem ser colocadas em um trait object comum.
  • move em make_discount é obrigatório. Se faltasse, a closure pegaria emprestada percent, que desapareceria quando a função terminasse.

4. Trait Iterator

4.1. O núcleo: a função next

Todo iterator em Rust gira em torno de um trait muito pequeno:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... dezenas de métodos prontos como map, filter, sum ...
}

next retorna Some(elemento) até esgotar os dados, depois retorna None. Um loop for é na verdade apenas sintaxe açucarada para chamar next repetidamente.

4.2. iter, iter_mut e into_iter

Uma collection pode produzir três tipos diferentes de iterator:

fn main() {
    let mut prices = vec![100, 200, 300];

    for p in prices.iter() {          // p: &i32
        print!("{p} ");
    }
    println!();

    for p in prices.iter_mut() {      // p: &mut i32
        *p += 10;
    }

    for p in prices.into_iter() {     // p: i32, vetor foi movido
        print!("{p} ");
    }
    println!();
    // println!("{:?}", prices); // erro: valor emprestado após move
}
100 200 300 
110 210 310 
  • iter() oferece referências imutáveis. A collection ainda pode ser usada depois.
  • iter_mut() oferece referências mutáveis para modificar elementos no local.
  • into_iter() toma propriedade. Escrever for p in prices também chama into_iter() implicitamente.

5. Adaptadores e consumidores

Os métodos em iterator se dividem em dois grupos:

  • Adaptadores (map, filter, take, zip, enumerate…) transformam um iterator em outro. Eles são lazy: não executam nada até que haja um consumidor.
  • Consumidores (collect, sum, count, fold, for_each, find…) realmente chamam next e produzem um resultado.

Usaremos a struct Order como dados de exemplo:

#[derive(Debug)]
pub struct Order {
    pub id: u32,
    pub customer: String,
    pub total: u32,
    pub paid: bool,
}

pub fn unpaid_total(orders: &[Order]) -> u32 {
    orders.iter()
        .filter(|o| !o.paid)
        .map(|o| o.total)
        .sum()
}

pub fn big_customers(orders: &[Order], threshold: u32) -> Vec<&str> {
    orders.iter()
        .filter(|o| o.total >= threshold)
        .map(|o| o.customer.as_str())
        .collect()
}

fn main() {
    let orders = vec![
        Order { id: 1, customer: "An".into(),   total: 150_000, paid: true },
        Order { id: 2, customer: "Bình".into(), total: 80_000,  paid: false },
        Order { id: 3, customer: "Chi".into(),  total: 320_000, paid: false },
    ];

    println!("Não coletado: {}", unpaid_total(&orders));
    println!("Grandes clientes: {:?}", big_customers(&orders, 100_000));

    let summary = orders.iter()
        .fold(String::new(), |acc, o| acc + &format!("#{} ", o.id));
    println!("Pedidos: {summary}");
}
Não coletado: 400000
Grandes clientes: ["An", "Chi"]
Pedidos: #1 #2 #3 

Os passos no exemplo:

  • filter(|o| !o.paid) recebe uma closure que retorna bool. O parâmetro o tem tipo &&Order, porque filter passa uma referência ao elemento, e o elemento de iter() já é &Order. Auto-deref permite que você escreva o.paid normalmente.
  • map(|o| o.total) transforma cada pedido em um valor monetário. sum() soma todos eles.
  • collect() agrupa o resultado em uma collection. O tipo de destino é inferido do tipo de retorno Vec<&str> da função.
  • fold recebe um valor inicial e uma closure (acc, elemento) -> acc_novo. Este é o consumidor mais geral.

5.1. Computação lazy

O trecho a seguir mostra que adaptadores só executam quando consumidos:

fn main() {
    let ids = vec![1, 2, 3];
    let doubled = ids.iter().map(|x| {
        println!("map {x}");
        x * 2
    });

    println!("Nada foi executado ainda");
    let result: Vec<i32> = doubled.collect();
    println!("{result:?}");
}
Nada foi executado ainda
map 1
map 2
map 3
[2, 4, 6]

Graças à computação lazy, a corrente filter → map → sum percorre os dados apenas uma vez e não cria collections intermediárias. O compilador geralmente otimiza esse pipeline para código de máquina equivalente a um loop escrito manualmente. Esta é a razão pela qual iterator é chamado de zero-cost abstraction.

6. Escrevendo seu próprio iterator

Você só precisa implementar next. Todos os outros adaptadores vêm de graça. O exemplo a seguir gera números de fatura no formato NF-0001:

pub struct InvoiceNumbers {
    current: u32,
    end: u32,
}

impl InvoiceNumbers {
    pub fn new(start: u32, end: u32) -> Self {
        Self { current: start, end }
    }
}

impl Iterator for InvoiceNumbers {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current > self.end {
            return None;
        }
        let number = format!("NF-{:04}", self.current);
        self.current += 1;
        Some(number)
    }
}

fn main() {
    let customers = ["An", "Bình", "Chi"];

    for (invoice, customer) in InvoiceNumbers::new(7, 100).zip(customers) {
        println!("{invoice} -> {customer}");
    }
}
NF-0007 -> An
NF-0008 -> Bình
NF-0009 -> Chi

Explicação:

  • type Item = String declara o tipo do elemento que o iterator retorna.
  • next atualiza o estado através de &mut self e retorna None quando excede end.
  • zip emparelha dois iterators em tuplas e para quando o iterator mais curto termina. Por isso apenas três linhas são impressas, mesmo que o intervalo vá até 100.

7. Escrevendo testes

Rust tem um framework de teste integrado. Você coloca o módulo de teste no mesmo arquivo que unpaid_total e InvoiceNumbers:

#[cfg(test)]
mod tests {
    use super::*;

    fn order(id: u32, total: u32, paid: bool) -> Order {
        Order { id, customer: format!("CL{id}"), total, paid }
    }

    #[test]
    fn unpaid_total_only_counts_unpaid_orders() {
        let orders = vec![order(1, 100, true), order(2, 50, false), order(3, 70, false)];
        assert_eq!(unpaid_total(&orders), 120);
    }

    #[test]
    fn unpaid_total_of_empty_slice_is_zero() {
        assert_eq!(unpaid_total(&[]), 0);
    }

    #[test]
    fn invoice_numbers_stop_after_end() {
        let numbers: Vec<String> = InvoiceNumbers::new(9, 11).collect();
        assert_eq!(numbers, vec!["NF-0009", "NF-0010", "NF-0011"]);
    }
}
cargo test
running 3 tests
test tests::invoice_numbers_stop_after_end ... ok
test tests::unpaid_total_of_empty_slice_is_zero ... ok
test tests::unpaid_total_only_counts_unpaid_orders ... ok

test result: ok. 3 passed; 0 failed; 0 ignored

O segundo teste verifica um caso extremo. sum() em um iterator vazio retorna 0, sem panic.

8. Erros comuns e quando não usar

Nota: Esquecer o consumidor é o erro mais comum. A linha orders.iter().map(|o| println!("{}", o.id)); não imprime nada. O compilador avisa “iterators are lazy and do nothing unless consumed”. Quando você precisa apenas de efeitos colaterais, use um loop for ou for_each.

Alguns outros erros que você encontrará:

  • collect não sabe o tipo de destino. Escrever let v = it.collect(); resulta em erro “type annotations needed”. Você precisa ser explícito: let v: Vec<_> = ... ou use turbofish collect::<Vec<_>>().
  • Usar into_iter() e depois tentar usar a collection novamente. Se você precisa apenas ler, use iter().
  • Closure segura o empréstimo muito tempo. Uma closure FnMut capturando &mut cart impede que você leia cart até a última chamada da closure. Restrinja o escopo da closure ou chame-a primeiro e depois leia.
  • Precisar de ? dentro de map. O operador ? em uma closure retorna apenas da closure, não da função externa. Use collect::<Result<Vec<_>, _>>(). Isso para no primeiro erro e retorna Err.

Quando não forçar tudo em uma corrente de iterator:

  • Lógica com muitas ramificações, múltiplos pontos de break/continue, ou atualizando várias variáveis ao mesmo tempo. Um loop for explícito é mais legível do que um fold de dez linhas.
  • Você precisa retornar closures de múltiplas ramificações, mas se preocupa com desempenho. Box<dyn Fn> tem overhead de alocação e chamada dinâmica. Considere usar enum ou genéricos em vez disso.

9. Conclusão

Closures em Rust capturam variáveis da forma mais leve possível: empréstimo, empréstimo mutável, ou move. A partir daí, a closure implementa automaticamente Fn, FnMut ou FnOnce. Ao escrever uma função que recebe closure, escolha o trait mais permissivo que a função precisa.

Iterator se baseia em apenas um método next. Adaptadores como map, filter, zip são lazy, enquanto consumidores como collect, sum, fold realmente executam o pipeline. Combinando essas duas ferramentas, você escreve código conciso, seguro em memória e tão rápido quanto loops manuais. Você também deve saber quando um simples loop for é a melhor escolha.

rustiteratorclosurefunctional programming