Funções e closures avançadas
Esta seção explora alguns recursos avançados relacionados a funções e closures, incluindo ponteiros de função e o retorno de closures.
Ponteiros de função
Já falamos sobre como passar closures para funções; você também pode passar
funções comuns para outras funções! Essa técnica é útil quando você quer passar uma
função que já definiu em vez de definir uma nova closure. Funções
coagem para o tipo fn (com f minúsculo), não confundir com a
trait de closure Fn. O tipo fn é chamado de ponteiro de função. Passar
funções por meio de ponteiros de função permite que você use funções como argumentos
para outras funções.
A sintaxe para especificar que um parâmetro é um ponteiro de função é semelhante à
das closures, como mostrado na Listagem 20-28, em que definimos uma função
add_one que adiciona 1 ao seu parâmetro. A função do_twice recebe dois
parâmetros: um ponteiro de função para qualquer função que receba um parâmetro i32
e retorne um valor i32, além de um valor i32. A função do_twice chama a
função f duas vezes, passando-lhe o valor arg, e então soma os resultados
dessas duas chamadas. A função main chama do_twice com os argumentos
add_one e 5.
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
fn para aceitar um ponteiro de função como argumentoEsse código imprime The answer is: 12. Especificamos que o parâmetro f em
do_twice é um fn que recebe um parâmetro do tipo i32 e retorna um
i32. Podemos então chamar f no corpo de do_twice. Em main, podemos passar
o nome da função add_one como o primeiro argumento para do_twice.
Ao contrário de closures, fn é um tipo, e não uma trait, portanto especificamos fn como
tipo do parâmetro diretamente, em vez de declarar um parâmetro genérico com
uma das traits Fn como bound.
Os ponteiros de função implementam as três closure traits (Fn, FnMut e
FnOnce), o que significa que você sempre pode passar um ponteiro de função como argumento para uma
função que espera uma closure. Em geral, é melhor escrever funções usando um tipo
genérico e uma das closure traits para que suas funções possam aceitar
tanto funções quanto closures.
Dito isso, um caso em que você pode querer aceitar apenas fn, e não
closures, é ao fazer interface com código externo que não possui closures: funções em C
podem aceitar funções como argumentos, mas C não tem closures.
Como exemplo de onde você pode usar tanto uma closure definida inline quanto uma
função nomeada, vamos observar o uso do método map, fornecido pela trait
Iterator na biblioteca padrão. Para usar map para transformar um vetor de
números em um vetor de strings, poderíamos usar uma closure, como na Listagem 20-29.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
map para converter números em stringsOu poderíamos passar uma função nomeada como argumento para map, em vez de uma closure.
A Listagem 20-30 mostra como isso ficaria.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
String::to_string com o método map para converter números em stringsObserve que precisamos usar a sintaxe totalmente qualificada de que falamos na
seção “Traits avançadas” porque existem
múltiplas funções disponíveis com o nome to_string.
Aqui, estamos usando a função to_string definida na trait ToString,
que a biblioteca padrão implementou para qualquer tipo que implemente
Display.
Lembre-se da seção “Valores de enum”, no Capítulo 6: o nome de cada variante de enum que definimos também se torna uma função inicializadora. Podemos usar essas funções inicializadoras como ponteiros de função que implementam as closure traits, o que significa que podemos passá-las como argumentos para métodos que recebem closures, como visto na Listagem 20-31.
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
map para criar uma instância de Status a partir de númerosAqui, criamos instâncias Status::Value usando cada valor u32 do intervalo
sobre o qual map é chamado, por meio da função inicializadora Status::Value.
Algumas pessoas preferem esse estilo, e outras preferem usar closures. Os dois
compilam para o mesmo código, então use o estilo que for mais claro para você.
Retornando closures
Closures são representadas por traits, o que significa que você não pode retorná-las
diretamente. Na maioria dos casos em que você quer retornar uma trait, pode
usar o tipo concreto que a implementa como valor de retorno da
função. No entanto, isso normalmente não funciona com closures porque elas não
têm um tipo concreto retornável; por exemplo, você não pode usar o ponteiro de
função fn como tipo de retorno se a closure capturar algum valor do próprio
escopo.
Em vez disso, normalmente você usará a sintaxe impl Trait, que aprendemos no
Capítulo 10. Você pode retornar qualquer tipo de função usando Fn, FnOnce e FnMut.
Por exemplo, o código da Listagem 20-32 compila sem problemas.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
impl TraitNo entanto, como observamos na seção “Inferência e anotação de tipos de closure”, no Capítulo 13, cada closure também é um tipo distinto. Se você precisar trabalhar com várias funções que têm a mesma assinatura, mas implementações diferentes, precisará usar um trait object para elas. Considere o que acontece se você escrever um código como o da Listagem 20-33.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T> de closures definidas por funções que retornam tipos impl FnAqui temos duas funções, returns_closure e returns_initialized_closure,
que ambas retornam impl Fn(i32) -> i32. Observe que as closures que elas
retornam são diferentes, embora implementem a mesma trait. Se tentarmos
compilar isso, Rust nos informará que não funcionará:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
A mensagem de erro nos diz que, sempre que retornamos um impl Trait, Rust
cria um tipo opaco exclusivo: um tipo cujos detalhes não podemos ver
nem nomear diretamente. Portanto, mesmo que essas funções retornem closures que implementam
a mesma trait, Fn(i32) -> i32, os tipos opacos que Rust gera para cada uma são
distintos. (Isso é semelhante à forma como Rust produz tipos concretos diferentes para
blocos async distintos, mesmo quando eles têm o mesmo tipo de saída, como vimos em
“O tipo Pin e a trait Unpin” no
Capítulo 17.) Já vimos uma solução para esse problema algumas vezes: podemos
usar um trait object, como na Listagem 20-34.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
Vec<T> de closures definidas por funções que retornam Box<dyn Fn> para que tenham o mesmo tipoEsse código compila sem problemas. Para obter mais informações sobre trait objects, consulte a seção “Usando objetos de trait para abstrair comportamento compartilhado” no Capítulo 18.
A seguir, vamos ver macros!