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 19-27, 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.

Filename: src/main.rs

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}");
}

Listing 19-27: Usando el tipo fn para aceptar un puntero a function como un argumento

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: return an `impl Trait` instead of a `dyn Trait`, if all returned values are the same type
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~
help: 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 17.

¡Ahora veamos las macros!