Funciones y Closures Avanzados
Esta sección cubre algunas características avanzadas relacionadas con funciones y closures, incluyendo punteros a funciones y retornar closures.
Function Pointers
Hemos hablado de cómo pasar closures a funciones; ¡también puedes pasar
funciones regulares a funciones! Esta técnica es útil cuando quieres pasar una
función que ya has definido en lugar de definir un nuevo closure. Las funciones
se coercen al tipo fn (con una f minúscula), no confundir con el trait de
cierre Fn. El tipo fn se llama puntero a función. Pasar funciones con
punteros a función te permitirá usar funciones como argumentos para otras
funciones.
La sintaxis para especificar que un parámetro es un puntero a función es
similar a la de los closures, como se muestra en el Listado 20-28, donde hemos
definido una función add_one que suma uno a su parámetro. La función
do_twice toma dos parámetros: un puntero a función a cualquier función que
tome un parámetro i32 y devuelva un i32, y un valor i32. La función
do_twice llama a la función f dos veces, pasándole el valor arg, luego
suma los dos resultados de la llamada a la función. La función main llama a
do_twice con los argumentos add_one y 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}"); }
Este código imprime The answer is: 12. Especificamos que el parámetro f en
do_twice es un fn que toma un parámetro de tipo i32 y devuelve un i32.
Luego podemos llamar a f en el cuerpo de do_twice. En main, podemos pasar
el nombre de la función add_one como el primer argumento a do_twice.
A diferencia de los closures, fn es un tipo en lugar de un trait, por lo que
especificamos fn como el tipo de parámetro directamente en lugar de declarar
un parámetro de tipo genérico con uno de los traits Fn como un trait bound.
Los punteros a funciones implementan los tres closure traits (Fn, FnMut y
FnOnce), lo que significa que siempre puedes pasar un puntero a función como
un argumento para una función que espera un closure. Es mejor escribir
funciones usando un tipo generic y uno de los closure traits para que tus
funciones puedan aceptar funciones o closures.
Dicho esto, un ejemplo de dónde querrías aceptar solo fn y no closures es
cuando te comunicas con código externo que no tiene closures: las funciones de
C pueden aceptar funciones como argumentos, pero C no tiene closures.
Como ejemplo de dónde podrías usar un closure definido en línea o una función
nombrada, veamos un uso del método map proporcionado por el trait Iterator
en la biblioteca estándar. Para usar la función map para convertir un vector
de números en un vector de strings, podríamos usar un closure, como en el
Listado 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(); }
O podríamos nombrar una función como argumento para map en lugar del
closure. El Listado 20-30 muestra cómo se vería.
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(); }
Ten en cuenta que debemos utilizar la sintaxis completamente calificada que mencionamos anteriormente en la sección “Traits avanzados”
porque hay múltiples funciones disponibles llamadas `to_string`.Aquí, estamos usando la función to_string definida en el trait ToString,
que la biblioteca estándar ha implementado para cualquier tipo que implemente
Display.
Recuerda la sección “Valores de Enum” del Capítulo 6, que el nombre de cada variante de enum que definimos también se convierte en una función inicializadora. Podemos usar estas funciones inicializadoras como punteros a función que implementan los closure traits, lo que significa que podemos especificar las funciones inicializadoras como argumentos para los métodos que toman closures, como puedes ver en el Listado 20-31:
fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
Aquí creamos instancias de Status::Value usando cada valor u32 en el rango
en el que se llama a map usando la función inicializadora de Status::Value.
A algunas personas les gusta este estilo, y a otras les gusta usar closures.
Compilan al mismo código, así que usa el estilo que sea más claro para ti.
Retornando Closures
Las closures están representadas por traits, lo que significa que no puedes
retornar closures directamente. En la mayoría de los casos en los que podrías
querer devolver un trait, en su lugar puedes usar el tipo concreto que
implementa el trait como valor de retorno de la función. Sin embargo, no puedes
hacer eso con closures porque no tienen un tipo concreto que sea retornable; por
ejemplo, no está permitido usar el puntero a función fn como tipo de retorno.
En su lugar, normalmente usarás la sintaxis impl Trait que aprendimos en el
Capítulo 10. Puedes devolver cualquier tipo de función, usando Fn, FnOnce y
FnMut. Por ejemplo, el código en el Listado 20-32 funcionará perfectamente.
#![allow(unused)] fn main() { fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } }
Sin embargo, como mencionamos en “Inferencia y anotación de tipos en closures” en el Capítulo 13, cada closure también es un tipo distinto por sí mismo. Si necesitas trabajar con múltiples funciones que tienen la misma firma pero diferentes implementaciones, tendrás que usar un trait object para ellas. Considera qué sucede si escribes un código como el que se muestra en el Listado 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
}
Aquí tenemos dos funciones, returns_closure y returns_initialized_closure,
que ambas retornan impl Fn(i32) -> i32. Observa que los closures que devuelven
son diferentes, aunque implementan el mismo tipo. Si intentamos compilar esto,
Rust nos indica que no 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` (opaque type at <src/main.rs:9:25>)
found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
= 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
El mensaje de error nos indica que cada vez que retornamos un impl Trait,
Rust crea un tipo opaco único, un tipo cuyos detalles no podemos ver ni
conocer cómo Rust lo construye. Así, aunque estas funciones retornan closures
que implementan el mismo trait, Fn(i32) -> i32, los tipos opacos que Rust
genera para cada una son distintos. (Esto es similar a cómo Rust produce tipos
concretos diferentes para bloques async distintos, incluso cuando tienen el
mismo tipo de salida, como vimos en
“Trabajando con cualquier número de futuros” en el
Capítulo 17). Hemos visto una solución para este problema varias veces: podemos
usar un trait objeto, como en el Listado 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) }
Este código se compilará sin problemas. Para más información sobre trait objects, consulta la sección “Usando trait objects que permiten valores de diferentes tipos” en el Capítulo 18.
¡Ahora, veamos las macros!