Closures em Rust são funções anônimas que podem capturar variáveis do escopo onde são definidas. Elas são flexíveis, podem ser armazenadas em variáveis, passadas como argumentos e retornadas de funções. Nesta aula, exploraremos a sintaxe das closures, como elas capturam o ambiente, as três traits que as regem (Fn, FnMut, FnOnce) e o uso da palavra-chave move para transferir ownership.

Entender closures é fundamental para programação funcional em Rust, especialmente ao trabalhar com iteradores, callbacks e concorrência. Vamos mergulhar nos detalhes.

Sintaxe

Closures têm uma sintaxe concisa. A forma mais simples é |parâmetros| expressão. Se o corpo tiver múltiplas linhas, use chaves. Exemplo:

let soma = |a: i32, b: i32| -> i32 { a + b };
println!("{}", soma(2, 3)); // 5

Os tipos dos parâmetros e do retorno podem ser inferidos, mas podem ser explicitados. Se o corpo for uma única expressão, as chaves são opcionais:

let multiplica = |x, y| x * y;
println!("{}", multiplica(4, 5)); // 20

Capturando o ambiente

Closures podem capturar variáveis do escopo externo por referência imutável, referência mutável ou por valor (ownership). O compilador decide automaticamente o tipo de captura com base no uso da variável dentro da closure.

Exemplo de captura por referência imutável:

let nome = String::from("Alice");
let saudacao = || println!("Olá, {}", nome);
saudacao(); // Olá, Alice
println!("{}", nome); // ainda válido

Se a closure modificar a variável, ela capturará por referência mutável:

let mut contador = 0;
let mut incrementa = || contador += 1;
incrementa();
println!("{}", contador); // 1

Se a variável for movida para dentro da closure (por exemplo, usando move), ela é capturada por valor.

Fn, FnMut, FnOnce

Toda closure implementa pelo menos uma das traits: Fn, FnMut ou FnOnce. Elas determinam como a closure pode ser chamada e o que ela pode fazer com as variáveis capturadas.

  • FnOnce: Pode ser chamada uma vez. Consome as variáveis capturadas. Toda closure implementa FnOnce.
  • FnMut: Pode ser chamada múltiplas vezes e pode modificar o ambiente capturado (referência mutável).
  • Fn: Pode ser chamada múltiplas vezes sem modificar o ambiente (referência imutável). É a mais restritiva.

O compilador escolhe a trait mais específica que a closure pode implementar. Por exemplo, uma closure que só lê variáveis implementa Fn (e por consequência FnMut e FnOnce).

Exemplo:

fn executa_fn(f: impl Fn()) { f(); }
fn executa_fn_mut(mut f: impl FnMut()) { f(); }
fn executa_fn_once(f: impl FnOnce()) { f(); }

let x = 10;
let closure = || println!("{}", x);
executa_fn(closure);
executa_fn_mut(closure);
executa_fn_once(closure);

move

A palavra-chave move força a closure a tomar ownership das variáveis capturadas, em vez de emprestar referências. Isso é útil quando a closure precisa ser enviada para outra thread ou armazenada em uma estrutura que viva mais que o escopo original.

let s = String::from("Olá");
let closure = move || {
    println!("{}", s);
};
// println!("{}", s); // erro: s foi movido
closure();

Sem move, a closure capturaria s por referência, mas se a closure for chamada após s sair de escopo, causaria erro. move garante que a closure possua os dados.

Boas práticas

Prefira usar move apenas quando necessário, para evitar cópias desnecessárias. Em closures que serão passadas para funções que exigem FnOnce (como thread::spawn), move é obrigatório. Além disso, closures são frequentemente usadas com iteradores, como map, filter, for_each.

Referências

Exercícios

  1. Crie uma closure que receba dois inteiros e retorne a soma deles. Atribua a uma variável e chame-a com os valores 5 e 7. Imprima o resultado.

    ✓ Resposta:
    let soma = |a: i32, b: i32| -> i32 { a + b };
    println!("{}", soma(5, 7)); // 12
  2. Escreva uma closure que capture uma variável externa nome: String e a imprima. Use move para garantir que a closure possua a string. Depois de definir a closure, tente usar nome fora dela e veja o erro.

    ✓ Resposta:
    let nome = String::from("Maria");
    let closure = move || println!("{}", nome);
    closure();
    // println!("{}", nome); // erro: nome foi movido
  3. Crie uma função aplica_tres_vezes que recebe uma closure f e um valor x, e aplica f três vezes sobre x. Use a trait Fn para o parâmetro. Teste com uma closure que dobra o valor.

    ✓ Resposta:
    fn aplica_tres_vezes<F>(f: F, x: i32) -> i32
    where
        F: Fn(i32) -> i32,
    {
        f(f(f(x)))
    }
    
    fn main() {
        let dobra = |n| n * 2;
        println!("{}", aplica_tres_vezes(dobra, 1)); // 8
    }
  4. Crie uma closure que modifica uma variável externa total (mutável) adicionando um valor. Use FnMut e chame a closure duas vezes. Imprima o total após cada chamada.

    ✓ Resposta:
    let mut total = 0;
    let mut adiciona = |valor: i32| total += valor;
    adiciona(10);
    println!("{}", total); // 10
    adiciona(5);
    println!("{}", total); // 15
  5. Escreva uma função que recebe um vetor de números e uma closure f que implementa FnOnce (que consome o vetor) e aplica f ao vetor. Teste com uma closure que imprime a soma dos elementos.

    ✓ Resposta:
    fn processa_vetor<F>(v: Vec<i32>, f: F)
    where
        F: FnOnce(Vec<i32>),
    {
        f(v);
    }
    
    fn main() {
        let numeros = vec![1, 2, 3, 4, 5];
        processa_vetor(numeros, |v| {
            let soma: i32 = v.iter().sum();
            println!("{}", soma); // 15
        });
    }