Pular para o conteúdo

Closures em Rust

Uma closure (função anônima) é uma função pequena e sem nome que você pode guardar em uma variável e passar de um lado para o outro. O que a diferencia de uma função comum é que ela consegue “capturar” variáveis do escopo em que foi criada.

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

Saída:

6
▶ Experimente você mesmo

As barras verticais em |x: i32| fazem o papel dos parênteses que uma função comum usaria. Compare as duas lado a lado:

fn add_one_fn(x: i32) -> i32 { x + 1 } // uma função comum
let add_one_closure = |x: i32| x + 1; // a mesma coisa como closure

Muitas vezes o Rust também consegue deduzir os tipos do parâmetro e do retorno, então é comum ver closures escritas de forma ainda mais curta:

let add_one = |x| x + 1;

Uma closure sem parâmetros usa as barras vazias: let say_hi = || println!("oi");, e você a chama com say_hi().

Se houver mais de uma expressão, coloque o corpo entre chaves, como em uma função:

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "par" } else { "ímpar" };
format!("{n} é {kind}")
};
println!("{}", describe(7));
}

Saída:

7 é ímpar
▶ Experimente você mesmo

O format! funciona como o println!, mas, em vez de imprimir o texto, ele o devolve como uma String. Aqui, essa String é a última expressão dentro das chaves, então é ela que a closure retorna.

Este é o recurso que uma função comum não tem: uma closure pode usar variáveis do lugar onde foi definida, sem que você precise passá-las explicitamente:

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

Saída:

90
40
▶ Experimente você mesmo

O discount não é um parâmetro da closure: ele só está ali no escopo da main, e a closure o alcança e usa diretamente. Uma fn comum não consegue fazer isso; ela precisaria receber discount como argumento.

Uma closure também pode alterar uma variável que ela captura, desde que essa variável seja mut. Existe só uma regra a mais: a própria closure também precisa ser guardada em uma variável mut. Chamá-la muda alguma coisa, e o Rust quer que isso fique visível no seu código.

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("a contagem agora é {count}");
};
increment();
increment();
increment();
println!("contagem final: {count}");
}

Saída:

a contagem agora é 1
a contagem agora é 2
a contagem agora é 3
contagem final: 3
▶ Experimente você mesmo

Se você esquecer o mut em increment, o compilador diz exatamente o que está errado: calling increment requires mutable binding due to mutable borrow of count. Ou seja, chamar increment exige uma variável mutável, porque a closure pega count emprestado de forma mutável.

Tem um detalhe que vale a pena conhecer. Enquanto a closure ainda puder ser chamada, ela tem o direito de alterar count, então ninguém mais pode mexer em count. Se você colocar um println!("{count}"); entre duas chamadas de increment(), recebe error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable: não dá para pegar count emprestado para leitura porque ele já está emprestado para alteração. Depois da última chamada de increment(), a closure terminou o serviço, e count volta a ser todo seu. É por isso que a linha da contagem final funciona. Você vai conhecer a história completa por trás dessa regra na página de Borrowing (empréstimo).

Às vezes uma closure precisa ficar com o ownership (posse) completo do que captura, por exemplo quando ela é entregue a outra thread. Para isso, você escreve move antes das barras: move || { ... }. Você vai ver isso em ação na página de Concorrência.

Você não vai usar closures sozinhas com muita frequência. Onde elas são realmente úteis é para passar um comportamento personalizado para outra função, principalmente para os métodos de iteradores que você vai conhecer na página de Iteradores:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

Saída:

[2, 4, 6, 8, 10]
▶ Experimente você mesmo

Aqui, |n| n * 2 é uma closure passada direto para o .map(). Esse padrão (uma closure pequena comandando um método como .map() ou .filter()) é onde você vai usá-las de verdade no dia a dia.

Crie uma variável tax_percent com o valor 20. Depois, escreva uma closure with_tax que recebe um preço e devolve o preço mais o imposto, usando o tax_percent capturado. Imprima o resultado para os preços 50 e 120. Dica: o imposto é price * tax_percent / 100.

Ver solução
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 com imposto dá {}", with_tax(50));
println!("120 com imposto dá {}", with_tax(120));
}

Saída:

50 com imposto dá 60
120 com imposto dá 144
▶ Experimente você mesmo

O tax_percent nunca é passado como argumento. A closure o captura do main, que é exatamente o que uma fn comum não conseguiria fazer.

Escreva uma closure next_id que devolve 1 na primeira vez que você a chama, 2 na segunda, 3 na terceira, e assim por diante. Chame-a três vezes e imprima cada ID. Dica: mantenha um contador mut fora da closure, some 1 a ele lá dentro e faça do contador a última expressão da closure, para que ele seja retornado.

Ver solução
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("o primeiro usuário recebe o id {}", next_id());
println!("o segundo usuário recebe o id {}", next_id());
println!("o terceiro usuário recebe o id {}", next_id());
}

Saída:

o primeiro usuário recebe o id 1
o segundo usuário recebe o id 2
o terceiro usuário recebe o id 3
▶ Experimente você mesmo

Tanto last_id quanto next_id precisam de mut: o primeiro porque o valor dele muda, e o segundo porque chamar a closure altera o que ela capturou.

  • Uma closure é uma função anônima pequena: |parâmetros| expressão.
  • Os tipos costumam ser deduzidos, então as closures geralmente são mais curtas do que uma fn equivalente.
  • Ao contrário de uma função comum, uma closure pode capturar variáveis do escopo em que foi definida.
  • Uma closure que altera uma variável capturada precisa ser guardada em uma variável mut, e ela pega essa variável emprestada enquanto estiver sendo usada.
  • As closures são mais úteis quando passadas para outras funções, principalmente métodos de iteradores como o .map().

Teste o que aprendeu

1. Que símbolos uma closure usa no lugar dos parênteses de uma função para os parâmetros?

2. O que uma closure consegue fazer que uma fn comum não consegue?

3. Segundo esta página, onde as closures são mais usadas na prática?

4. Uma closure faz count += 1 em uma variável capturada. Do que mais você precisa?

Pontuação: 0 / 4