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 e poderosas, permitindo escrever código conciso e expressivo. Nesta aula, exploraremos a sintaxe, como closures capturam o ambiente, as traits que controlam como a captura ocorre e o uso da palavra-chave move.

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

Sintaxe

Closures em Rust têm uma sintaxe leve: parâmetros opcionais entre pipes | seguidos por um corpo. O corpo pode ser uma expressão única (sem chaves) ou um bloco com chaves. O tipo dos parâmetros e do retorno pode ser inferido ou explicitamente anotado.

Exemplo básico:

let add_one = |x: i32| -> i32 { x + 1 };
let result = add_one(5); // 6

Se o corpo for uma única expressão, as chaves podem ser omitidas:

let add_one = |x| x + 1;

Parâmetros múltiplos:

let add = |a, b| a + b;
println!("{}", add(2, 3)); // 5

É possível anotar tipos explicitamente, mas geralmente o compilador infere.

Capturando o ambiente

Closures podem capturar variáveis do escopo externo por referência imutável (&T), referência mutável (&mut T) ou por valor (T). O compilador escolhe a forma menos restritiva possível, baseada no uso da variável dentro do closure.

Exemplo de captura por referência imutável:

let s = String::from("hello");
let print_s = || println!("{}", s); // captura &s
print_s();
println!("{}", s); // ainda válido

Exemplo de captura por referência mutável:

let mut v = vec![1, 2, 3];
let mut push_to_v = |x| v.push(x); // captura &mut v
push_to_v(4);
// println!("{:?}", v); // erro: borrow mutável ativo

Exemplo de captura por valor (move):

let s = String::from("hello");
let consume_s = || drop(s); // captura s por valor
consume_s();
// println!("{}", s); // erro: s foi movido

Fn, FnMut, FnOnce

Rust categoriza closures por três traits: Fn, FnMut e FnOnce. Elas determinam como o closure pode ser chamado e quantas vezes.

  • FnOnce: Pode ser chamado uma vez. Captura variáveis por valor (move). O closure consome as variáveis.
  • FnMut: Pode ser chamado múltiplas vezes e pode modificar o ambiente (captura por referência mutável).
  • Fn: Pode ser chamado múltiplas vezes sem modificar o ambiente (captura por referência imutável).

O compilador implementa automaticamente a trait menos restritiva possível. Por exemplo, um closure que só lê variáveis implementa Fn (e também FnMut e FnOnce).

Exemplo com funções que aceitam diferentes traits:

fn call_once(f: impl FnOnce()) {
    f();
}
fn call_mut(mut f: impl FnMut()) {
    f();
    f();
}
fn call_fn(f: impl Fn()) {
    f();
    f();
}

let s = String::from("hello");
let c = || println!("{}", s);
call_fn(&c);
call_mut(&mut c);
call_once(c); // move

Note que Fn é subtrait de FnMut, que é subtrait de FnOnce. Portanto, um closure que implementa Fn pode ser usado onde se espera FnMut ou FnOnce.

move

A palavra-chave move força o closure a capturar variáveis por valor, transferindo a propriedade para o closure. Isso é útil quando o closure precisa sobreviver ao escopo da variável original, por exemplo, em threads ou retornos de função.

Exemplo:

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

Sem move, o closure capturaria s por referência, e se o closure for enviado para uma thread, a referência pode se tornar inválida. Com move, garantimos que o closure possui os dados.

Outro uso comum é em iteradores com map ou filter quando queremos capturar variáveis por valor:

let nums = vec![1, 2, 3];
let multiplier = 2;
let result: Vec<_> = nums.into_iter().map(move |x| x * multiplier).collect();
println!("{:?}", result); // [2, 4, 6]

Boas práticas

  • Prefira capturar por referência quando possível para evitar clonagem desnecessária.
  • Use move quando o closure precisar ser enviado para outra thread ou armazenado em uma estrutura que sobreviva ao escopo.
  • Lembre-se de que closures que implementam Fn são as mais flexíveis; tente projetar suas APIs para aceitar Fn quando possível.
  • Para closures que modificam o ambiente, use FnMut e garanta que a variável capturada seja mutável.

Referências

Exercícios

  1. Crie um closure que receba dois inteiros e retorne o maior deles. Atribua a uma variável e chame-o com valores 10 e 20.

    ✓ Resposta:
    let max = |a: i32, b: i32| if a > b { a } else { b };
    println!("{}", max(10, 20)); // 20
  2. Escreva um closure que capture uma variável String por referência imutável e a imprima. Depois, chame o closure e também use a variável original.

    ✓ Resposta:
    let s = String::from("hello");
    let print_s = || println!("{}", s);
    print_s();
    println!("{}", s); // ainda válido
  3. Crie um closure que modifique um vetor (por exemplo, adicione um elemento) usando FnMut. Atribua a uma variável mutável e chame-o duas vezes.

    ✓ Resposta:
    let mut v = vec![1, 2, 3];
    let mut push = |x| v.push(x);
    push(4);
    push(5);
    println!("{:?}", v); // [1, 2, 3, 4, 5]
  4. Escreva um closure que capture uma String por valor usando move e a descarte com drop. Depois, tente usar a variável original (não compile).

    ✓ Resposta:
    let s = String::from("hello");
    let drop_s = move || drop(s);
    drop_s();
    // println!("{}", s); // erro: s foi movido
  5. Crie uma função apply_twice que aceite um closure Fn(i32) -> i32 e um inteiro, e aplique o closure duas vezes ao inteiro. Use-a com um closure que dobra o valor.

    ✓ Resposta:
    fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
        f(f(x))
    }
    
    let double = |x| x * 2;
    println!("{}", apply_twice(double, 5)); // 20