Closures - Cierres

Los cierres o closures (en inglés) son una especie de funciones rápidas que no necesitan un nombre. En ocasiones se les denomina funciones lambdas. Son fáciles de encontrar en el código debido a que utilizan || en lugar de (). Son muy habituales en Rust y una vez se aprenden a utilizar uno se pregunta como ha podido vivir sin ellos.

Se puede asociar un closure a una variable y en ese caso funciona y parece exactamente como una función:

fn main() {
    let my_closure = || println!("Esto es un cierre");
    my_closure();
}

Este closure no recibe nada coomo parámetro || e imprime el mensaje Esto es un cierre.

Entre los símbolos || se pueden añadir variables de entrada y tipos, como se hace en las funciones dentro de ():

fn main() {
    let my_closure = |x: i32| println!("{}", x);

    my_closure(5);
    my_closure(5+5);
}

Imprime:

5
10

Cuando un cierre se hace más complejo, se puede escribir con un bloque de código que puede ser tan largo como se requiera:

fn main() {
    let my_closure = || {
        let number = 7;
        let other_number = 10;
        println!("Los dos números son {} y {}.", number, other_number);
          // Este cierre puede ser tan largo como se necesite, como sucede con las funciones.
    };

    my_closure();
}

Pero los cierres son especiales porque pueden guardarse valores de variables que se encuentren fuera del ellos incluso aunque no reciban parámetros (es decir, incluso habiendo escrito ||). Por ello, se puede escribir:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = || println!("{}", number_one + number_two);
    my_closure();
}

Este código imprime 16. No ha sido necesario añadir parámetros a || porque se pueden usar las variables dentro del código del cierre y sumarlas ahí.

Por cierto, por eso se llaman closures. Porque pueden tomar unas variables y encerrarlas dentro. Esto significa que si se quiere hablar con precisión:

  • Una || que no encierra ninguna variable exterior en su interior es una función anónima. No es, estrictamente, un closure.
  • Una || que sí encierra una variable exterior en su interior sí es un cierre.

Sin embargo, esta precisión no se suele hacer en el habla coloquial. Así que en este texto simplemente se llaman cierres a todos los ||.

¿Por qué es bueno conocer la diferencia? Porque las funciones anónimas generan realmente el mismo código que el de las funciones con nombre. Aunque pueda parecer que el código máquina de una función anónima será más complejo, no lo es. Es igual (y, por lo tanto, igual de rápido) que el código de una función habitual.

Para qué pueden servir los cierres. Por ejemplo:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = |x: i32| println!("{}", number_one + number_two + x);
    my_closure(5);
}

Este cierre guarda en su interior a dos variables number_one y number_two. Pero también recibe como parámetro una nueva variable x y cuando se ejecuta, se le pasa 5 al parámetro x. Suma los tres valores y, en este caso, devuelve 21.

Normalmente, se usan los cierres para pasarlos como parámetros a determinados métodos. En la última sección se usaron cierres con .map()y .for_each(). En esa sección, se escribió |x| para obtener el siguiente elemento en un iterador; eso era un cierre.

Otro ejemplo: el método unwrap_or que se usa para asignar un valor cuando no funciona unwrap. Igual que se puede escribir la siguiente función let fourth = my_vec.get(3).unwrap_or(&0);, existe también el método unwrap_or_else que recibe un cierre como parámetro. Por ejemplo:

fn main() {
    let my_vec = vec![8, 9, 10];

    let fourth = my_vec.get(3).unwrap_or_else(|| { // Intenta "desenvolver". Si no funciona:
        if my_vec.get(0).is_some() {               // observa si el vector contiene algo en el índice [0]
            &my_vec[0]                             // Utiliza ese valor si existe algo.
        } else {
            &0 // en otro caso, usa el &0
        }
    });

    println!("{}", fourth);
}

Por supuesto, los cierres pueden ser muy simples. Se puede escribir, simplemente let fourth = my_vec.get(3).unwrap_or_else(|| &0);. No siempre se necesita usar {} y escribir código complejo solo porque es un cierre. Solo con escribir || el compilador conoce que hay un cierre.

Quizás el uso más habitual de un cierre sea en la función .map(). A continuación se muestra otro ejemplo:

fn main() {
    let num_vec = vec![2, 4, 6];

    let double_vec = num_vec        // usa el vector
        .iter()                     // para recorrerlo (iterar)
        .map(|number| number * 2)   // para cada elemento, lo multiplica por 2
        .collect::<Vec<i32>>();     // y crea un nuevo vector a partir de ello
    println!("{:?}", double_vec);
}

Otro buen ejemplo es su uso con .for_each() después de .enumerate(). El método .enumerate() devuelve un iterador con el número de índice del elemento y el propio elemento. Por ejemplo, de [10, 9, 8 ] se obtiene (0, 10), (1, 9), (2, 8). Con los tipos (usize, i32).

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()      // itera sobre num_vec
        .enumerate() // obtiene pares de (índice, valor)
        .for_each(|(index, number)| println!("El índice {} contiene el valor {}", index, number)); // imprime para cada elemento
}

Este código imprime:

El índice 0 contiene el valor 10
El índice 1 contiene el valor 9
El índice 2 contiene el valor 8

En este caso se usa for_each en lugar de map. map es para ejecutar algo en cada elemento y pasarlo al siguiente paso (es un iterador en sí mismo) y actua de forma perezosa, es decir, si no hay nadie a quien pasarle el valor, no hace nada. for_each realiza la operación cuando recibe cada elemento. Por eso en caso de map hace falta usar un método como collect para obtener un resultado.

Esta es una característica muy interesante de los iteradores. Si se intenta ejecutar un map sin utilizar un método como collect, el compilador observará que ese código no hace nada. No fallará, pero lo indicará:

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()
        .enumerate()
        .map(|(index, number)| println!("El índice {} contiene el valor {}", index, number));

}

Mostrará:

warning: unused `std::iter::Map` that must be used
 --> src\main.rs:4:5
  |
4 | /     num_vec
5 | |         .iter()
6 | |         .enumerate()
7 | |         .map(|(index, number)| println!("Index number {} has number {}", index, number));
  | |_________________________________________________________________________________________^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

Se trata de un aviso warning, no es un error. El programa funciona correctamente. Pero no suma nada. Si se observa el tipo del resultado de cada fila, se entenderá:

  • let num_vec = vec![10, 9, 8]; En este momento se tiene un Vec<i32>.
  • .iter() El valor de retorno de esta función es un Iter<i32>. Es decir, un iterador sobre elementos de tipo i32.
  • .enumerate() Ahora es de tipo enumerado Enumerate<Iter<i32>>. Es decir, un Enumerate sobre un Iter de i32s.
  • .map() Y ahora el tipo es Map<Enumerate<Iter<i32>>>. Es decir, un Map sobre un Enumerate sobre un Iter de i32s.

Hasta el momento, todo lo que han hecho las funciones es ir construyendo una estructura compleja que está lista para usarse, solo que hay que decirle de algún modo que lo haga. Rust hace todo esto porque necesita ejecutarse rápido. No quiere hacer:

  • itera sobre todos los i32 en el vector
  • luego enumera todos los i32 del iterador
  • luego ejecuta la función sobre todos los i32

Rust realiza un único "bucle" solo cuando lo necesita. En el momento que se ejecuta una función como .collect::<Vec<i32>>() Rust sabe que se le está pidiendo construir un vector y comienza a pedir los diferentes valores. Esto es lo que significa que los iteradores son perezosos y no hacen nada a no ser que se consuman.

Se pueden crear cosas muy complejas como HashMap mediante el uso de .collect(). A continuación se muestra un ejemplo sobre como construir un HashMap a partir de dos vectores. En primer lugar, se construyen ambos vectores. Después se usará into_iter() para obtener un interador sobre los valores. Posteriormente se usará .zip() para unir ambos iteradores como un una cremallera, emparejando cada elemento de un iterador con el obtenido del otro. Por último, se utilizará .collect() para crear el HashMap:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // un Vec<i32>
    let some_words = vec!["cero", "uno", "dos", "tres", "cuatro", "cinco"]; // un Vec<&str>

    let number_word_hashmap = some_numbers
        .into_iter()                 // obtiene un iter
        .zip(some_words.into_iter()) // se combinan con .zip() dos iter.
        .collect::<HashMap<_, _>>();

    println!("Para la clave {} se obtiene {}.", 2, number_word_hashmap.get(&2).unwrap());
}

Que imprime:

Para la clave 2 se obtiene dos.

Se observa que se escribió <HashMap<_, _>>, que es información suficiente para que Rust sepa decidir el tipo del objeto como <HashMap<i32, &str>>. También habría sido posible haber escrito .collect::<HashMap<i32, &str>>();. E incluso así:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // un Vec<i32>
    let some_words = vec!["cero", "uno", "dos", "tres", "cuatro", "cinco"]; // un Vec<&str>
    let number_word_hashmap: HashMap<_, _> = some_numbers  // Puesto que se indica el tipo aquí...
        .into_iter()
        .zip(some_words.into_iter())
        .collect(); // no es necesario indicarlo aquí
}

Hay otro método que es como .enumerate(), pero para el tipo char: .char_indices(). Se usa de la misma forma. Si se supone que se tiene una cadena que está formada por números de tres dígitos:

fn main() {
    let numbers_together = "140399923481800622623218009598281";

    for (index, number) in numbers_together.char_indices() {
        match (index % 3, number) {
            (0..=1, number) => print!("{}", number), // solo imprime el número si hay resto
            _ => print!("{}\t", number), // en otro caso, imprime el número y un tabulador
        }
    }
}

Esto imprime: 140 399 923 481 800 622 623 218 009 598 281.

|_| en un cierre (closure)

En ocasiones se puede ver |_| en un cierre. Esto significa que el cierre (closure) necesita un argumento (como podría ser una variable x), pero no se va a usar. Así que esta expresión en los parámetros de un cierre significa: vale, el cierre recibe un parámetro, pero no le doy un nombre porque no lo uso".

A continuación, se muestra un ejemplo que da error por no expresar el parámetro:

fn main() {
    let my_vec = vec![8, 9, 10];

    println!("{:?}", my_vec.iter().for_each(|| println!("No usamos la variable que se necesita"))); // ⚠️
}

Rust da un error que dice:

error[E0593]: closure ¿Está expected to take 1 argument, but it takes 0 arguments
 --> src/main.rs:4:36
  |
4 |     println!("{:?}", my_vec.iter().for_each(|| println!("No usamos la variable que se necesita"))); // ⚠️
  |                                    ^^^^^^^^ -- takes 0 arguments
  |                                    |
  |                                    expected closure that takes 1 argument

El propio compilador da alguna pista:

help: consider changing the closure to take and ignore the expected argument
  |
4 |     println!("{:?}", my_vec.iter().for_each(|_| println!("No usamos la variable que se necesita"))); // ⚠️
  |                                             ~~~

Es decir, que se debe pasar el argumento que necesita cualquier cierre que se pase como parámetro a la función for_each.

Si se cambia de este modo, funcionará:

fn main() {
    let my_vec = vec![8, 9, 10];

    println!("{:?}", my_vec.iter().for_each(|_| println!("No usamos la variable que se necesita")));
}

Métodos útiles para su uso con cierres e iteradores

Rust es un lenguaje muy divertido cuando se dominan los cierres. Con ellos se pueden encadenar métodos para hacer mucho con poco código. A continuación, se muestrans algunos métodos que se utilizan con cierres que aún no se habían visto:

.filter(): a partir de un iterador, permite conservar los elementos que se deseen. El siguiente ejemplo filtra los meses del año:

fn main() {
    let meses = vec!["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];

    let filtered_meses = meses
        .into_iter()                         // crea un iterador
        .filter(|month| month.len() <= 5)     // Solo los meses con cinco o menos caracters (byte)
                                             // En este caso, todos los caracteres son de un byte, por eso funciona usar .len()
        .filter(|month| month.contains("u")) // Se seleccionan solo los meses que contengan la letra u
        .collect::<Vec<&str>>();

    println!("{:?}", filtered_meses);
}

Esto imprime: ["Junio", "Julio"].

.filter_map(): este método realiza de una vez la función de .filter() y .map(). El cierre que se pasa como parámetro,debe devolver un Option<T>. El método filter_map() extraerá T para aquellos elementos que tengan valor (que sean de tipo Some<T>) y desechará los que sean de tipo None. Por ejemplo, el uso de .filter_map() con un vector como vec![Some(2), None, Some(3)] devuelve [2, 3].

A continuación se muestra un ejemplo con un struct de una Empresa. Cada empresa tiene un campo nombre de tipo String, y un campo ceo para describir a su responsable. El ceo puede haber dimitido, por lo que es de tipo Option<String> En el ejemplo, se filtran las empresas para mostrar el valor de los CEO.

struct Empresa {
    nombre: String,
    ceo: Option<String>,
}

impl Empresa {
    fn new(nombre: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        }; // Se crea el valor del campo ceo y debajo se crea el elemento struct
        Self {
            nombre: nombre.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone() // Devuelve un clone del CEO (struct no es tipo Copy)
    }
}

fn main() {
    let empresa_vec = vec![
        Empresa::new("Umbrella Corporation", "Unknown"),
        Empresa::new("Ovintiv", "Doug Suttles"),
        Empresa::new("The Red-Headed League", ""),
        Empresa::new("Stark Enterprises", ""),
    ];

    let all_the_ceos = empresa_vec
        .into_iter()
        .filter_map(|empresa| empresa.get_ceo()) // filter_map recibe Option<T> que es lo que necesita
        .collect::<Vec<String>>();

    println!("{:?}", all_the_ceos);
}

Esto imprime: ["Unknown", "Doug Suttles"]

Para facilitar el uso de .filter_map() con valores Result<T, E>, este último tiene el método .ok() que lo convierte en Option<T> usando None para los casos de error.

En el siguiente ejemplo se utiliza .parse() para convertir la entrada de usuario en números f32. En el resultado se registran los errores que pueda haber en la entrada del usuario, al usar .filter_map() junto con .ok() se descartan los valores erróneos.

fn main() {
    let user_input = vec!["8.9", "Nine point nine five", "8.0", "7.6", "eleventy-twelve"];

    let actual_numbers = user_input
        .into_iter()
        .filter_map(|input| input.parse::<f32>().ok())
        .collect::<Vec<f32>>();

    println!("{:?}", actual_numbers);
}

Lo anterior imprime: [8.9, 8.0, 7.6]

En sentido contrario de .ok(), los Option<T> disponen de los métodos .ok_or() y .ok_or_else(). Estas funcones convierten un Option<T> en Resutl<T, E>. La función .ok_or() devuelve un Ok o un Err, por lo que necesita que se le indique de qué tipo debe ser este error. Esto se debe a que los casos None de Option no disponen de información adicionales.

En estas funciones, .ok_or() y .ok_or_else(), el parámetro closure se utiliza para obtener el valor para los casos en los que el Option sea None.

En el siguiente ejemplo, se toma el Option del struct de Empresa para convertirlo en un Result. Para el manejo de errores a largo plazo, es bueno que la aplicación tenga sus propios tipos de error. En este caso solo se incluye un mensaje de error, por lo que el tipo del resultado es Result<String, &str>.

// El código es idéntico, salvo en la función main()
struct Empresa {
    nombre: String,
    ceo: Option<String>,
}

impl Empresa {
    fn new(nombre: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        };
        Self {
            nombre: nombre.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let empresa_vec = vec![
        Empresa::new("Umbrella Corporation", "Unknown"),
        Empresa::new("Ovintiv", "Doug Suttles"),
        Empresa::new("The Red-Headed League", ""),
        Empresa::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![]; // Para guardar los valores resultantes

    empresa_vec
        .iter()
        .for_each(|empresa| results_vec.push(empresa.get_ceo().ok_or("No CEO found")));

    for item in results_vec {
        println!("{:?}", item);
    }
}

El resultado de este código es el siguiente:

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found")
Err("No CEO found")

En este caso, se dispone de las cuatro entradas del usuario y se indica cuáles son erróneas.

A continuación se usa .ok_or_else() para que se pueda pasar como parámetro un cierre y obtener un mensaje de error mejor incluyendo el nombre de la empresa.

// El código es idéntico, salvo en la función main()
struct Empresa {
    nombre: String,
    ceo: Option<String>,
}

impl Empresa {
    fn new(nombre: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        };
        Self {
            nombre: nombre.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let empresa_vec = vec![
        Empresa::new("Umbrella Corporation", "Unknown"),
        Empresa::new("Ovintiv", "Doug Suttles"),
        Empresa::new("The Red-Headed League", ""),
        Empresa::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![]; // Para guardar los valores resultantes

    empresa_vec
        .iter()
        .for_each(|empresa| {
            results_vec.push(empresa.get_ceo().ok_or_else(|| {
                let mensaje_error = format!("No CEO found for {}", empresa.nombre);
                mensaje_error
            }))
        });

    for item in results_vec {
        println!("{:?}", item);
    }
}

El código anterior, imprime:

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found for The Red-Headed League")
Err("No CEO found for Stark Enterprises")

.and_then(): es un método que toma como parámetro un Option y permite realizar una operación con su valor pasando el resultado al siguiente método que exista. El resultado también debe ser de tipo Option. Es una forma segura de obtener el contenido del elemento Some, realizar la operación deseada y encapsular el resultado de nuevo en un Option.

Un ejemplo sencillo se puede ver a partir del resultado de la operación .get() sobre un vector, puesto que devuelve un Option. En este caso, se obtiene un número, se realiza un cálculo y se vuelve a convertir a Option. Los valores None se pasan sin procesar, siguen siendo None.

fn main() {
    let new_vec = vec![8, 9, 0]; // solo un vec con números
    let number_to_add = 5;       // Se usa en el cálculo más adelante
    let mut empty_vec = vec![];  // El resultado va aquí


    for index in 0..5 {
        empty_vec.push(
            new_vec
               .get(index)
                .and_then(|number| Some(number + 1))
                .and_then(|number| Some(number + number_to_add))
        );
    }
    println!("{:?}", empty_vec);
}

Esto imprime: [Some(14), Some(15), Some(6), None, None]. Se observa que no se filtran los None.

.and(): es parecido a bool para Option. Se pueden encadenar muchos y si todos son Some el resultado será el del último. Si existe algún None, el resultado será None.

Se muestra en primer lugar un ejemplo con booleanos para ayudar a entender el funcionamiento de .and() en base al funcionamiento de &&(and). Un solo false lo convierte todo en false.

fn main() {
    let one = true;
    let two = false;
    let three = true;
    let four = true;

    println!("{}", one && three); // prints true
    println!("{}", one && two && three && four); // prints false
}

A continuación, se muestra el mismo tipo de ejemplo con la función .and(). Se puede imagina que se ejecutaron cinco operaciones y para cada una de ellas, si el resultado fue correcto se guardar en un Vec<Option<&str>> un valor Some("éxito"). Se repiten tres veces estas cinco operaciones. Después se usa .and() para comparar si en cada operación se obtubo resultado en todas las ocasiones.

fn main() {
    let first_try = vec![Some("éxito"), None, Some("éxito"), Some("éxito"), None];
    let second_try = vec![None, Some("éxito"), Some("éxito"), Some("éxito"), Some("éxito")];
    let third_try = vec![Some("éxito"), Some("éxito"), Some("éxito"), Some("éxito"), None];

    for i in 0..first_try.len() {
        println!("{:?}", first_try[i].and(second_try[i]).and(third_try[i]));
    }
}

El resultado es el siguiente:

None
None
Some("éxito")
Some("éxito")
None

La cadena de .and() de la primera operación en los tres intentos devuelve None ya que el segundo intento tiene None en la primera posición (índice cero). La segunda operación también es None debido a que el primer intento dio un resultado None. La tercera operación sí tiene éxito debido a que los tres intentos tiene un resultado distinto de None.

.any() y .all(). Se utilizan de forma sencilla en iteradores. Devuelven un bool en función del parámetro de entrada. En el siguiente ejemplo, se crea un vector muy grande, de unos 20.000 elementos, con toods los caracteres desde la 'a' a la '働'. Después, se comprueba si contiene un determinado carácter.

A continuación, se crea un vector más pequeño y se comprueba si es totalmente alfabético (con .is_alphabetic()). POr último, se comprueba si todos los caracteres son menores que el carácter coreano '행'.

Se debe observar que se usan referencias allí donde es necesario ya que es lo que entrega .iter().

fn in_char_vec(char_vec: &Vec<char>, check: char) {
    println!("¿Está {} dentro? {}", check, char_vec.iter().any(|&char| char == check));
}

fn main() {
    let char_vec = ('a'..'働').collect::<Vec<char>>();
    in_char_vec(&char_vec, 'i');
    in_char_vec(&char_vec, '뷁');
    in_char_vec(&char_vec, '鑿');

    let smaller_vec = ('A'..'z').collect::<Vec<char>>();
    println!("¿Es todo alfabético? {}", smaller_vec.iter().all(|&x| x.is_alphabetic()));
    println!("¿Es todo menor que el carácter 행? {}", smaller_vec.iter().all(|&x| x < '행'));
}

Lo anterior imprime:

¿Está i dentro? true
¿Está 뷁 dentro? false
¿Está 鑿 dentro? false
¿Es todo alfabético? false
¿Es todo menor que el carácter 행? true

Por cierto, .any() solo comprueba hasta que encuentra un elemento que coincida. Funciona en modo cortocircuito. No sigue comprobando si ya ha encontrado una coincidencia. Por este motivo, si se va a usar esta función .any() con un Vec, puede ser buena idea que los elementos que puedan coincidir estén lo más cerca del inicio del iterador. Por ejemplo, usando .rev() después de .iter() si pueden estar por el final. Por ejemplo:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);
}

Que es un vector con 1000 números 6 seguidos de un 5. Si se quiere comprobar si contiene el número 5, lo óptimo sería obtener un iterador en sentido inverso con .rev().

En primer lugar, se puede revisar el funcionamiento esta función .rev()con el siguiente código:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut iterator = big_vec.iter().rev();
    println!("{:?}", iterator.next());
    println!("{:?}", iterator.next());
}

Que imprime:

Some(5)
Some(6)

Por lo tanto, lo eficiente, en este caso, para buscar un 5 sería buscarlo a través del iterador que comienza por el final:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    println!("{:?}", big_vec.iter().rev().any(|&number| number == 5));
}

Así, solo necesita iterar a través del primer elemento y luego se para. Si no se usara, en este ejemplo se iteraría a través de los 1001 elementos. El siguiente código permite observarlo:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut counter = 0; // Inicia la cuenta
    let mut big_iter = big_vec.into_iter(); // Crea un Iterator

    loop {
        counter +=1;
        if big_iter.next() == Some(5) { // Continua llamando a .next() hasta que obtiene un Some(5)
            break;
        }
    }
    println!("Final counter is: {}", counter);
}

Esto imprime Final counter is: 1001, por lo que queda claro que llamó a .next() 1001 veces antes de encontrar el 5.

.find() indica si un iterador contiene algo y .position() indica dónde está. .find() es diferente de .any() en que devuelve un Option que cotiene el valor buscado en un Some o None. .position(), por otra parte devuelve un Option con un Someque contiene el número de posición o None. En otras palabras:

  • .find(): intentará extraer el valor.
  • .position(): intentará indicar la posición en la que está el valor.

A continuación, se muestra un ejemplo simple:

fn main() {
    let num_vec = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100];

    println!("{:?}", num_vec.iter().find(|&number| number % 3 == 0)); // find takes a reference, so we give it &number
    println!("{:?}", num_vec.iter().find(|&number| number * 2 == 30));

    println!("{:?}", num_vec.iter().position(|&number| number % 3 == 0));
    println!("{:?}", num_vec.iter().position(|&number| number * 2 == 30));
}

Este código imprime:

Some(30) // Devuelve el primer valor que cumple la condición
None // No hay ningún número que multplicado por 2 == 30
Some(2) // Esta es la posición del elemneto
None

.cycle() puede crear un iterador infinito que nunca termina. Este tipo de iterador funciona muy bien con .zip() para crear un objeto nuevo Vec<(i32, &str)>.

fn main() {
    let even_odd = vec!["par", "impar"];
    let even_odd_vec = (0..6)
        .zip(even_odd.into_iter().cycle())
        .collect::<Vec<(i32, &str)>>();
    println!("{:?}", even_odd_vec);
}

Aunque .cycle() devuelve un iterador que no acaba nunca, en este caso, el iterador de .zip() solo se ejecuta seis veces antes de acabarse (cuando se agota el iterable de mayor longitud). La salida de este programa es:

[(0, "par"), (1, "impar"), (2, "par"), (3, "impar"), (4, "par"), (5, "impar")]

Algo parecido se puede consguir que no tiene fin. Si se escribe 0.. se crea un rango con un límite superior infinito y que nunca se completa. Es fácil de utilizar:

fn main() {
    let ten_chars = ('a'..).take(10).collect::<Vec<char>>();
    let skip_then_ten_chars = ('a'..).skip(1300).take(10).collect::<Vec<char>>();

    println!("{:?}", ten_chars);
    println!("{:?}", skip_then_ten_chars);
}

Ambos imprimen 10 caracteres, pero el segundo se salta 1.300 lugares e imprime diez letras en Armenio.

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
['յ', 'ն', 'շ', 'ո', 'չ', 'պ', 'ջ', 'ռ', 'ս', 'վ']

.fold() es otro popular método que se utliza para sumar elementos de un iterador, pero se puede hacer mucho más. Es similar en cierta medida a .for_each(). En .fold() primero se añade un valor inicial (que puede ser 0 para poder sumar los valores), seguido del cierre. El cierre recibe dos parámetros, el valor total hasta el momento y el siguiente elemento. Se ve a continuación un ejemplo simple para utlizar .fold() para sumar elementos.

fn main() {
    let some_numbers = vec![9, 6, 9, 10, 11];

    println!("{}", some_numbers
        .iter()
        .fold(0, |total_so_far, next_number| total_so_far + next_number)
    );
}

Así:

  • En el paso 1, comienza con 0 y suma el siguiente número: 9.
  • En el paso 2, toma el 9 y le suma el 6: 15.
  • En el paso 3, toma el 15 y le suma el 9: 24.
  • En el paso 4, toma el 24 y le suma el 10: 34.
  • En el paso 5, toma el 34 y le suma el 11: 45, se imprime 45.

Pero no es necesario usarlo solo para sumar. En el siguiente ejemplo se añade '-' a cada carácter para construir una cadena de caracteres de tipo String.

fn main() {
    let a_string = "No tengo guiones.";

    println!(
        "{}",
        a_string
            .chars() // iterator sobre cada carácter
            .fold("-".to_string(), |mut string_so_far, next_char| { // Comienza con una "-"
                string_so_far.push(next_char); // Incorpora el siguiente caracter, seguido de '-'
                string_so_far.push('-');
                string_so_far} // y se pasa al siguiente bucle
            ));
}

Que imprime:

-N-o- -t-e-n-g-o- -g-u-i-o-n-e-s-.-

Hay muchos otros métodos de gran utilidad como:

  • .take_while() que va leyendo elementos de un iterador mientras retorna true.
  • .cloned() que crea un clones de los elementos del iterador. Convierte una referencia en un valor.
  • .by_ref() que convierte en referencias los elementos del iterador. Es bueno para asegurarse de que se puede usar un Vec o similar después de que se haya iterado a través de él.
  • Existen muchos métodos acabados en _while: .skip_while(), .map_while().
  • .sum(): simplemente suma todos los elementos en un solo valor.

.chunks() y .windows() son dos formas de partir un vector en el tamaño que se desee. Se indica el tamñao que se desea como parámetro. Por ejemplo, si se tiene un vector de 10 elementos [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], y se pasa un parámetro de 3, se debe trabajar de la siguiente forma:

  • .chunk() dará lugar a cuatro fragmentos (slices): [0, 1, 2]. [3, 4, 5], [6, 7, 8] y [9]. El parámetro fija el número de elementos en cada fragmento. Con un posible fragmento de tamaño inferior al final.
  • .windows() dará lugar a más fragmentos en este caso ya que creará fragmentos del tamaño indicado y se desplazará de uno en uno: [0, 1, 2], [1, 2, 3], [2, 3, 4], ...., [7, 8, 9].

Se observa el funcionamiento con el siguiente código:

fn main() {
    let num_vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

    for chunk in num_vec.chunks(3) {
        println!("{:?}", chunk);
    }
    println!();
    for window in num_vec.windows(3) {
        println!("{:?}", window);
    }
}

Que imprime:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[0]

[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 0]

Hay que tener en cuenta que .chunks() lanzará panic! si no se le indica ningún tamaño. Se puede escribir .chunks(1000) para un vector de un solo elemento, pero no se puede escribir .chunks() con algo de longitud 0. La función valida assert!(chunk_size != 0).

.match_indices() permite extraer de un String o &str todo lo que coincida con el parámetro e indica el índice también. Es similar a .enumerate() en que retorna una tupla con dos elementos.

fn main() {
    let rules = "Regla número 1: No luchar. Regla número 2: ve a la cama a las 8 pm. Regla número 3: levántate a las 6 am.";
    let rule_locations = rules.match_indices("Regla").collect::<Vec<(_, _)>>(); // Es Vec<usize, &str>
    println!("{:?}", rule_locations);
}

Que imprime:

[(0, "Regla"), (28, "Regla"), (70, "Regla")]

.peekable() crea un iterador que permite echar un vistazo al siguiente elemento. Es como llmar a .next()(devuelve un Option) solo que sin avanzar el iterador. Se puede usar tanto como se necesite. A continuación, se muestra un ejemplo de uso de .peek() tres veces para cada elemento.

fn main() {
    let just_numbers = vec![1, 5, 100];
    let mut number_iter = just_numbers.iter().peekable(); // Esto crea un tipo de iterator denominado Peekable

    for _ in 0..3 {
        println!("Me encanta el número {}", number_iter.peek().unwrap());
        println!("Me encanta tanto el número {}", number_iter.peek().unwrap());
        println!("{} es un número tan bonito", number_iter.peek().unwrap());
        number_iter.next();
    }
}

Que imprime:

Me encanta el número 1
Me encanta tanto el número 1
1 es un número tan bonito
Me encanta el número 5
Me encanta tanto el número 5
5 es un número tan bonito
Me encanta el número 100
Me encanta tanto el número 100
100 es un número tan bonito

A continuación se muestra otro ejemplo que usa .peek() para contrastar con match previamente a llamar a .next().

fn main() {
    let locations = vec![
        ("Nevis", 25),
        ("Taber", 8428),
        ("Markerville", 45),
        ("Cardston", 3585),
    ];
    let mut location_iter = locations.iter().peekable();
    while location_iter.peek().is_some() {
        match location_iter.peek() {
            Some((name, number)) if *number < 100 => { // .peek() da una referencia, se necesita *
                println!("Encontrada una aldea: {} con {} personas", name, number)
            }
            Some((name, number)) => println!("Encontrado un pueblo: {} con {} personas", name, number),
            None => break,
        }
        location_iter.next();
    }

El código anterior da como resultado:

Encontrada una aldea: Nevis con 25 personas
Encontrado un pueblo: Taber con 8428 personas
Encontrada una aldea: Markerville con 45 personas
Encontrado un pueblo: Cardston con 3585 personas

Finalmente, se muestra un ejemplo en que se se usa también .match_indices(). En este caso, introducen nombres en un struct dependiendo del número de espacios de la &str.

#[derive(Debug)]
struct Nombres {
    una_palabra: Vec<String>,
    dos_palabras: Vec<String>,
    tres_palabras: Vec<String>,
}

fn main() {
    let vec_of_Nombres = vec![
        "Caesar",
        "Frodo Baggins",
        "Bilbo Baggins",
        "Jean-Luc Picard",
        "Data",
        "Rand Al'Thor",
        "Paul Atreides",
        "Barack Hussein Obama",
        "Bill Jefferson Clinton",
    ];

    let mut iter_of_Nombres = vec_of_Nombres.iter().peekable();

    let mut all_Nombres = Nombres { // inicia un struct sin nombres
        una_palabra: vec![],
        dos_palabras: vec![],
        tres_palabras: vec![],
    };

    while iter_of_Nombres.peek().is_some() {
        let next_item = iter_of_Nombres.next().unwrap(); // Se puede usar .unwrap() ya que se sabe que contiene algo
        match next_item.match_indices(' ').collect::<Vec<_>>().len() { // Crea un vector utilizando .match_indices y comprueba la longitud
            0 => all_Nombres.una_palabra.push(next_item.to_string()),
            1 => all_Nombres.dos_palabras.push(next_item.to_string()),
            _ => all_Nombres.tres_palabras.push(next_item.to_string()),
        }
    }

    println!("{:?}", all_Nombres);
}

El código anterior imprime:

Nombres { una_palabra: ["Caesar", "Data"], dos_palabras: ["Frodo Baggins", "Bilbo Baggins", "Jean-Luc Picard", "Rand Al'Thor", "Paul Atreides"], tres_palabras: ["Barack Hussein Obama", "Bill Jefferson Clinton"] }