Closures
As closures de Rust são funções anônimas que você pode armazenar em uma variável ou passar como argumentos para outras funções. Você pode criar uma closure em um lugar e depois chamá-la em outro para avaliá-la em um contexto diferente. Ao contrário das funções, closures podem capturar valores do escopo em que são definidas. Vamos demonstrar como esses recursos de closures permitem reutilizar código e personalizar comportamentos.
Capturando o Ambiente
Primeiro, vamos examinar como podemos usar closures para capturar valores do ambiente em que são definidas para uso posterior. O cenário é o seguinte: de tempos em tempos, nossa empresa de camisetas distribui uma camiseta exclusiva, de edição limitada, para alguém da nossa lista de e-mails como promoção. As pessoas na lista de e-mails podem opcionalmente adicionar sua cor favorita ao perfil. Se a pessoa escolhida para ganhar a camiseta tiver uma cor favorita definida, ela recebe uma camiseta dessa cor. Se não tiver especificado uma cor favorita, ela recebe a cor da qual a empresa tem mais unidades no momento.
Há muitas formas de implementar isso. Neste exemplo, vamos usar um enum
chamado ShirtColor, com as variantes Red e Blue para simplificar o
número de cores disponíveis. Representamos o estoque da empresa com uma struct
Inventory, que tem um campo chamado shirts contendo um
Vec<ShirtColor> que representa as cores das camisetas atualmente em estoque.
O método giveaway, definido em Inventory, recebe a preferência opcional de
cor da pessoa sorteada e retorna a cor da camiseta que ela vai receber. Essa
configuração é mostrada na Listagem 13-1.
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
O store definido em main tem duas camisetas azuis e uma vermelha restantes
para distribuir nessa promoção de edição limitada. Chamamos o método
giveaway para uma pessoa que prefere camiseta vermelha e para outra sem
nenhuma preferência.
Mais uma vez, esse código poderia ser implementado de várias maneiras, e aqui,
para manter o foco em closures, ficamos com conceitos que você já aprendeu,
exceto pelo corpo do método giveaway, que usa uma closure. No método
giveaway, recebemos a preferência do usuário como um parâmetro do tipo
Option<ShirtColor> e chamamos o método unwrap_or_else em
user_preference. O método unwrap_or_else em Option<T>
uma closure sem parâmetros que retorna um valor T (o mesmo tipo armazenado
na variante Some de Option<T>, neste caso ShirtColor). Se o Option<T>
for a variante Some, unwrap_or_else retorna o valor dentro de Some. Se
for a variante None, unwrap_or_else chama a closure e retorna o valor
produzido por ela.
Especificamos a expressão de closure || self.most_stocked() como argumento de
unwrap_or_else. Essa é uma closure que não recebe parâmetros (se tivesse,
eles apareceriam entre as duas barras verticais). O corpo da closure chama
self.most_stocked(). Estamos definindo a closure aqui, e a implementação de
unwrap_or_else vai avaliá-la depois, se o resultado for necessário.
A execução deste código imprime o seguinte:
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
Um aspecto interessante aqui é que passamos uma closure que chama
self.most_stocked() na instância atual de Inventory. A biblioteca padrão
não precisou saber nada sobre os tipos Inventory ou ShirtColor que
definimos, nem sobre a lógica que queremos usar nesse cenário. A closure
captura uma referência imutável à instância self de Inventory e a passa,
junto com o código que especificamos, para o método unwrap_or_else.
Funções, por outro lado, não conseguem capturar o ambiente dessa maneira.
Inferindo e Anotando Tipos de Closures
Há mais diferenças entre funções e closures. Em geral, closures não exigem que
você anote os tipos dos parâmetros nem o valor de retorno, como acontece com
as funções fn. Anotações de tipo são necessárias em funções porque os tipos
fazem parte de uma interface explícita exposta a quem usa seu código. Definir
essa interface rigidamente é importante para garantir que todos concordem
sobre quais tipos de valores uma função usa e retorna. Closures, por outro
lado, não são usadas em uma interface exposta dessa forma: elas ficam
armazenadas em variáveis e são usadas sem receber um nome e sem serem expostas
às pessoas usuárias da nossa biblioteca.
Closures normalmente são curtas e relevantes apenas dentro de um contexto restrito, em vez de em qualquer cenário arbitrário. Dentro desses contextos limitados, o compilador consegue inferir os tipos dos parâmetros e o tipo de retorno, de maneira semelhante ao que faz com a maioria das variáveis (existem casos raros em que o compilador também precisa de anotações de tipo em closures).
Assim como acontece com variáveis, podemos adicionar anotações de tipo se quisermos tornar o código mais explícito e claro, ao custo de deixá-lo mais verboso do que o estritamente necessário. Anotar os tipos de uma closure ficaria como na definição mostrada na Listagem 13-2. Neste exemplo, estamos definindo uma closure e armazenando-a em uma variável, em vez de defini-la no próprio lugar em que a passamos como argumento, como fizemos na Listagem 13-1.
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
Com as anotações de tipo adicionadas, a sintaxe de closures fica mais parecida com a sintaxe de funções. Aqui, definimos uma função que soma 1 ao seu parâmetro e uma closure com o mesmo comportamento, para fins de comparação. Adicionamos alguns espaços para alinhar as partes relevantes. Isso ilustra como a sintaxe de closures é parecida com a de funções, exceto pelo uso das barras verticais e pela quantidade de sintaxe que é opcional:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
A primeira linha mostra uma definição de função, e a segunda mostra uma
definição de closure com anotações completas. Na terceira linha, removemos as
anotações de tipo da definição da closure. Na quarta, removemos as chaves, que
são opcionais porque o corpo da closure tem apenas uma expressão. Todas essas
são definições válidas que produzirão o mesmo comportamento quando forem
chamadas. As linhas add_one_v3 e add_one_v4 exigem que as closures sejam
avaliadas para que possam compilar, porque os tipos serão inferidos com base
no uso. Isso é semelhante ao fato de let v = Vec::new(); precisar de
anotações de tipo ou de valores de algum tipo inseridos em Vec para que Rust
consiga inferir o tipo.
Para definições de closures, o compilador vai inferir um tipo concreto para
cada parâmetro e para o valor de retorno. Por exemplo, a Listagem 13-3 mostra
a definição de uma closure curta que simplesmente retorna o valor que recebe
como parâmetro. Essa closure não é muito útil fora do contexto deste exemplo.
Observe que não adicionamos nenhuma anotação de tipo à definição. Como não há
anotações de tipo, podemos chamar a closure com qualquer tipo, e aqui fizemos
isso pela primeira vez com String. Se depois tentarmos chamar
example_closure com um inteiro, teremos um erro.
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
O compilador nos dá este erro:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
Na primeira vez que chamamos example_closure com o valor String, o
compilador infere que o tipo de x e o tipo de retorno da closure são
String. Esses tipos então ficam fixados na closure em example_closure, e
recebemos um erro de tipo quando tentamos usar um tipo diferente com a mesma
closure.
Capturando Referências ou Movendo Ownership
Closures podem capturar valores do ambiente de três maneiras, que correspondem diretamente às três maneiras pelas quais uma função pode receber um parâmetro: emprestando de forma imutável, emprestando de forma mutável e tomando ownership. A closure decidirá qual dessas abordagens usar com base no que o corpo da função faz com os valores capturados.
Na Listagem 13-4, definimos uma closure que captura uma referência imutável ao
vetor chamado list, porque ela só precisa de uma referência imutável para
imprimir o valor.
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
Este exemplo também ilustra que uma variável pode se ligar a uma definição de closure, e depois podemos chamar a closure usando o nome da variável e parênteses, como se o nome da variável fosse o nome de uma função.
Como podemos ter várias referências imutáveis a list ao mesmo tempo, list
continua acessível no código antes da definição da closure, depois da
definição mas antes da chamada, e depois que a closure é chamada. Esse código
compila, executa e imprime:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
A seguir, na Listagem 13-5, alteramos o corpo da closure para que ela adicione
um elemento ao vetor list. Agora a closure captura uma referência mutável.
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
Este código compila, executa e imprime:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Observe que não há mais um println! entre a definição e a chamada da closure
borrows_mutably: quando borrows_mutably é definida, ela captura uma
referência mutável a list. Não usamos a closure de novo depois que ela é
chamada, então o empréstimo mutável termina ali. Entre a definição da closure
e sua chamada, não é permitido fazer um empréstimo imutável para imprimir,
porque nenhum outro empréstimo é permitido enquanto existe um empréstimo
mutável. Tente adicionar um println! ali para ver qual mensagem de erro você
obtém!
Se quiser forçar a closure a tomar ownership dos valores que ela usa do
ambiente, mesmo quando o corpo da closure não precisa estritamente de
ownership, você pode usar a palavra-chave move antes da lista de parâmetros.
Essa técnica é especialmente útil quando passamos uma closure para uma nova
thread, movendo os dados para que passem a pertencer à nova thread. Vamos
discutir threads e por que você pode querer usá-las em detalhes no Capítulo
16, quando falarmos sobre concorrência. Por enquanto, vamos apenas explorar
brevemente a criação de uma nova thread usando uma closure que precisa da
palavra-chave move. A Listagem 13-6 mostra a Listagem 13-4 modificada para
imprimir o vetor em uma nova thread em vez de na thread principal.
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
move para forçar a closure da thread a tomar ownership de listCriamos uma nova thread e passamos para ela uma closure a ser executada. O
corpo da closure imprime a lista. Na Listagem 13-4, a closure capturava list
apenas por meio de uma referência imutável, porque esse era o menor nível de
acesso necessário para imprimi-la. Neste exemplo, embora o corpo da closure
ainda precise apenas de uma referência imutável, precisamos especificar que
list deve ser movido para dentro da closure, colocando a palavra-chave
move no início da definição. Se a thread principal executasse mais operações
antes de chamar join na nova thread, a nova thread poderia terminar antes do
resto da thread principal, ou a thread principal poderia terminar primeiro. Se
a thread principal mantivesse ownership de list, mas terminasse antes da nova
thread e descartasse list, a referência imutável na thread ficaria inválida.
Por isso, o compilador exige que list seja movido para a closure passada à
nova thread, para que a referência continue válida. Tente remover a palavra
move ou usar list na thread principal depois que a closure for definida
para ver quais erros do compilador você recebe!
Movendo Valores Capturados para Fora de Closures
Depois que uma closure capturou uma referência ou tomou ownership de um valor do ambiente em que foi definida, o código em seu corpo determina o que acontece com essas referências ou valores quando a closure for avaliada mais tarde. Isso afeta o que, se é que algo é, movido para dentro e para fora da closure.
O corpo de uma closure pode fazer qualquer uma das seguintes coisas: mover para fora da closure um valor capturado, modificar o valor capturado, não mover nem modificar o valor, ou nem sequer capturar algo do ambiente.
A forma como uma closure captura e trata valores do ambiente afeta quais traits
ela implementa, e traits são a maneira como funções e structs podem especificar
que tipos de closures conseguem usar. Closures implementam automaticamente uma,
duas ou as três traits Fn, de maneira acumulativa, dependendo de como o
corpo da closure lida com os valores:
FnOncese aplica a closures que podem ser chamadas uma vez. Todas as closures implementam pelo menos essa trait, porque todas podem ser chamadas. Uma closure que move valores capturados para fora do próprio corpo implementará apenasFnOnce, e nenhuma das outras traitsFn, porque só pode ser chamada uma vez.FnMutse aplica a closures que não movem valores capturados para fora do próprio corpo, mas podem modificar os valores capturados. Essas closures podem ser chamadas mais de uma vez.Fnse aplica a closures que não movem valores capturados para fora do próprio corpo nem modificam valores capturados, bem como a closures que não capturam nada do ambiente. Essas closures podem ser chamadas mais de uma vez sem modificar o ambiente, o que é importante em casos como chamar uma closure várias vezes de forma concorrente.
Vamos olhar a definição do método unwrap_or_else em Option<T>, que usamos
na Listagem 13-1:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Lembre-se de que T é o tipo genérico que representa o tipo do valor na
variante Some de um Option. Esse tipo T também é o tipo de retorno de
unwrap_or_else: um código que chama unwrap_or_else em um
Option<String>, por exemplo, receberá um String.
Agora, observe que a função unwrap_or_else tem um parâmetro de tipo genérico
adicional, F. O tipo F é o tipo do parâmetro chamado f, que é a closure
que fornecemos ao chamar unwrap_or_else.
O limite de trait especificado sobre o tipo genérico F é FnOnce() -> T, o
que significa que F precisa poder ser chamado uma vez, não receber
argumentos e retornar um T. Usar FnOnce no limite de trait expressa a
restrição de que unwrap_or_else não chamará f mais de uma vez. No corpo de
unwrap_or_else, vemos que, se o Option for Some, f não será chamado.
Se o Option for None, f será chamado uma vez. Como todas as closures
implementam FnOnce, unwrap_or_else aceita os três tipos de closures e é o
mais flexível possível.
Nota: se o que queremos fazer não exigir capturar um valor do ambiente, podemos usar o nome de uma função no lugar de uma closure quando precisarmos de algo que implemente uma das traits
Fn. Por exemplo, em um valorOption<Vec<T>>, poderíamos chamarunwrap_or_else(Vec::new)para obter um novo vetor vazio se o valor forNone. O compilador implementa automaticamente a traitFnapropriada para uma definição de função.
Agora, vamos observar o método sort_by_key, da biblioteca padrão, definido em
slices, para ver como ele difere de unwrap_or_else e por que usa FnMut, em
vez de FnOnce, como limite de trait. A closure recebe um argumento na forma
de uma referência ao item atual do slice que está sendo considerado e retorna
um valor do tipo K, que pode ser ordenado. Essa função é útil quando você
quer ordenar um slice por um atributo específico de cada item. Na Listagem
13-7, temos uma lista de instâncias de Rectangle e usamos sort_by_key para
ordená-las pelo atributo width, da menor para a maior.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
sort_by_key para ordenar retângulos pela larguraEste código imprime:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
A razão pela qual sort_by_key é definido para receber uma closure FnMut é
que ele a chama várias vezes: uma vez para cada item do slice. A closure
|r| r.width não captura, não modifica nem move nada para fora do seu
ambiente, então ela satisfaz os requisitos do limite de trait.
Em contraste, a Listagem 13-8 mostra um exemplo de closure que implementa
apenas a trait FnOnce, porque move um valor para fora do ambiente. O
compilador não vai permitir que usemos essa closure com sort_by_key.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
FnOnce com sort_by_keyEssa é uma forma artificial e complicada, que não funciona, de tentar contar o
número de vezes que sort_by_key chama a closure ao ordenar list. Esse
código tenta fazer a contagem inserindo value, uma String do ambiente da
closure, no vetor sort_operations. A closure captura value e então move
value para fora da closure ao transferir ownership de value para o vetor
sort_operations. Essa closure só pode ser chamada uma vez; tentar chamá-la
uma segunda vez não funcionaria, porque value não estaria mais no ambiente
para ser inserido em sort_operations novamente! Portanto, essa closure
implementa apenas FnOnce. Quando tentamos compilar esse código, recebemos o
erro de que value não pode ser movido para fora da closure porque a closure
precisa implementar FnMut:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
O erro aponta para a linha do corpo da closure que move value para fora do
ambiente. Para corrigir isso, precisamos mudar o corpo da closure para que ela
não mova valores para fora do ambiente. Manter um contador no ambiente e
incrementar seu valor no corpo da closure é uma forma mais direta de contar o
número de vezes que a closure é chamada. A closure da Listagem 13-9 funciona
com sort_by_key porque ela captura apenas uma referência mutável ao contador
num_sort_operations e, por isso, pode ser chamada mais de uma vez.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
FnMut com sort_by_key é permitidoAs traits Fn são importantes ao definir ou usar funções e tipos que fazem uso
de closures. Na próxima seção, vamos discutir iteradores. Muitos métodos de
iteradores recebem closures como argumento, então vale a pena manter esses
detalhes em mente enquanto avançamos!