Cierres en funciones

Los cierres (closures) son muy útiles. ¿Cómo los pasamos como parámetro a nuestras propias funciones?

Es posible hacerlo, pero dentro de ellas es necesario definir el tipo de cierre (de los tres tipos posibles vistos en el apartardo anterior). Fuera de una función, Rust decide por sí mismo qué tipo de cierre debe usar: Fn, FnMut o FnOnce. Sin embargo, dentro de la función resulta necesario seleccionar qué tipo se admite. Loa mejor forma de comprenderlo es revisar varias definiciones de funión. Por ejemplo, la siguiente de .all(). Si se recuerda, esta función comprueba si un iterador cumple una condición en todos sus elementos. Parte de la definición dice:

#![allow(unused)]
fn main() {
    fn all<F>(&mut self, f: F) -> bool    // 🚧
    where
        F: FnMut(Self::Item) -> bool,
}
  1. fn all<F> indica que existe un tipo genérico F. Un cierre siempre es un tipo genérico ya que cada función es en sí misma un tipo diferente (con un solo valor).
  2. (&mut self, f: F): &mut self revela que esta función es un método. f: F es lo que por convenio se suele escribir para representar un parámetro que debe recibir un cierre: este es el nombre de la variable y el tipo genérico. No es obligatorio, lógicamente, usar estas letras f y F.
  3. La siguiente parte es la que define que el genérico tiene que ser uno de los tres tipos de cierre: where F: FnMut(Self::Item) -> bool. Se requiere, en este caso, que el cierre sea modificable para que se puedan modificar sus parámetros. En este caso, necesita cambiar el iterador (Self::Item). Se devuelve un booleano: true o false.

A continuación se muestar una definición más simple con un cierre:

#![allow(unused)]
fn main() {
fn do_something<F>(f: F)    // 🚧
where
    F: FnOnce(),
{
    f();
}
}

En este caso, se toma un cierre como parámetro (se hace propietario de él ya que es de tipo FnOnce) y no devuelve ningún valor. El cierre, según se define en la cláusula where, no tiene parámetros y no devuelve ningún valor.

Ampliando el ejemplo anterior, se crea un Vec y se itera a través de él para mostrar lo que se puede hacer:

fn do_something<F>(f: F)
where
    F: FnOnce(),
{
    f();
}

fn main() {
    let some_vec = vec![9, 8, 10];
    do_something(|| {
        some_vec
            .into_iter()
            .for_each(|x| println!("The number is: {}", x));
    })
}

Para ver un ejemplo más realista, se crea a continuación un struct City. Esta vez tendrá más datos sobre años y población. Dispone de un Vec<u32> para contener todos los años y otro Vec<u32>igual para el número de habitantes.

City tiene dos funciones: new() para crear una nueva ciudad y .city_data() que recibe un cierre. Cuando se usa .city_data() devuelve los años, los habitantes y un cierre, para que se pueda realizar la operación que se solicite con los datos. El tipo del cierre es FnMut para poder modificar los valores. El ejemplo es así:

#[derive(Debug)]
struct City {
    name: String,
    years: Vec<u32>,
    populations: Vec<u32>,
}

impl City {
    fn new(name: &str, years: Vec<u32>, populations: Vec<u32>) -> Self {

        Self {
            name: name.to_string(),
            years,
            populations,
        }
    }

    fn city_data<F>(&mut self, mut f: F) // self. Solo f es genérico de tipo F. f es el cierre

    where
        F: FnMut(&mut Vec<u32>, &mut Vec<u32>), // El cierre toma como parámetrso dos vectors de u32
                                // que representan el año y la poblicación.
                                // no devuelve ningún valor
    {
        f(&mut self.years, &mut self.populations) // Finalmente este es el código de la función
            // simplemente usa el cierre en los dos parámetros year y habitantes"
            // se puede hacer lo que se quiera dentro del cierre. No devuelve ningún valor
    }
}

fn main() {
    let years = vec![
        1372, 1834, 1851, 1881, 1897, 1925, 1959, 1989, 2000, 2005, 2010, 2020,
    ];
    let populations = vec![
        3_250, 15_300, 24_000, 45_900, 58_800, 119_800, 283_071, 478_974, 400_378, 401_694,
        406_703, 437_619,
    ];
    // Se crea la ciudad
    let mut tallinn = City::new("Tallinn", years, populations);

    // Ahora se tiene el método .city_data() que utiliza un cierre.
    // Se puede hacer lo que se quiera.

    // En primer lugar se unen los 5  primeros años y los habitantes para imprimirlos.
    tallinn.city_data(|city_years, city_populations| { // Los parámetros se pueden llamar como se quiera
        let new_vec = city_years
            .into_iter()
            .zip(city_populations.into_iter()) // Zip los dos valores juntos
            .take(5)                           // pero se queda con los 5 primeros
            .collect::<Vec<(_, _)>>(); // Deja a Rust decidir el tipo de la tupla
        println!("{:?}", new_vec);
    });

    // Ahora se va a añadir algún dato para el año 2030
    tallinn.city_data(|x, y| { // Esta vez se les llama a los parámetros: x, y.
        x.push(2030);
        y.push(500_000);
    });

    // Ahora no se quieren los datos de 1834
    tallinn.city_data(|x, y| {
        let position_option = x.iter().position(|x| *x == 1834);
        if let Some(position) = position_option {
            println!(
                "Going to delete {} at position {:?} now.",
                x[position], position
            ); // Confirma que se está borrando el elemento apropiado
            x.remove(position);
            y.remove(position);
        }
    });

    println!(
        "Years left are {:?}\nPopulations left are {:?}",
        tallinn.years, tallinn.populations
    );
}

Esto imprimirá el valor de todas las veces que se ha llamado a .city_data()::

[(1372, 3250), (1834, 15300), (1851, 24000), (1881, 45900), (1897, 58800)]
Going to delete 1834 at position 1 now.
Years left are [1372, 1851, 1881, 1897, 1925, 1959, 1989, 2000, 2005, 2010, 2020, 2030]
Populations left are [3250, 24000, 45900, 58800, 119800, 283071, 478974, 400378, 401694, 406703, 437619, 500000]