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 este:
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, como este:
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 se muestra a continuación:
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
Los closures se representan mediante traits, lo que significa que no puedes
devolver closures directamente. En la mayoría de los casos en los que podrías
querer devolver un trait, puedes usar en su lugar el tipo concreto que
implementa el trait como el valor de retorno de la función. Sin embargo, no
puedes hacer eso con los closures porque no tienen un tipo concreto que se
pueda devolver; no se te permite usar el puntero a función fn
como un tipo
de retorno, por ejemplo.
El siguiente código intenta devolver un closure directamente, pero no compilará:
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
El error del compilador es el siguiente:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
help: consider returning an `impl Trait` instead of a `dyn Trait`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~
help: alternatively, box the return type, and wrap all of the returned values in `Box::new`
|
1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
2 ~ Box::new(|x| x + 1)
|
For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` (lib) due to 1 previous error
¡El error hace referencia nuevamente al trait Sized
! Rust no sabe cuánto
espacio necesitará para almacenar el closure. Vimos una solución a este
problema anteriormente. Podemos usar un trait object:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
Este código se compilará correctamente. Para obtener más información sobre los trait objects, consulta la sección “Usando trait objects que permiten valores de diferentes tipos”
en el Capítulo 19.¡Ahora veamos las macros!