Iteradores

Un iterador es una construcción del lenguaje Rust que recupera los elementos contenidos en una colección uno a uno. En realidad, ya se han utilizado de forma frecuente los iteradores en los capítulos anteriores: el bucle for entrega un iterador. Para obtener un iterador para otros usos, es necesario indicar el tipo que se requiere:

  • .iter() para iterar a través de referencias a los elementos.
  • .iter_mut() para iterar mediante referencias modificables (mutables).
  • .into_iter() para obtener un iterador sobre valores, no referencias.

Un bucle for es solo un iterador que es propietario de sus valores. Es por eso que se puede hacer modificable y se pueden cambiar los valores cuando se utiliza.

Los iteradores se pueden usar como sigue:

fn main() {
    let vector1 = vec![1, 2, 3]; // Se usará .iter() y .into_iter() sobre este vector
    let vector1_a = vector1.iter().map(|x| x + 1).collect::<Vec<i32>>();
    let vector1_b = vector1.into_iter().map(|x| x * 10).collect::<Vec<i32>>();

    let mut vector2 = vec![10, 20, 30]; // Se usará .iter_mut() sobre este otro vector
    vector2.iter_mut().for_each(|x| *x +=100);

    println!("{:?}", vector1_a);
    println!("{:?}", vector2);
    println!("{:?}", vector1_b);
}

Que imprime:

[2, 3, 4]
[110, 120, 130]
[10, 20, 30]

En los primeros dos casos se ha utilizado el método map() que permite ejecutar algún código sobre cada elemento antes de pasarlo a la siguiente función. En el último caso se hay utilizado el método for_each(). Este método ejecuta el código que contiene sobre cada elemento que recibe. El uso de .iter_mut() junto con for_each() equivalen a un bucle for. Dentro de cada método se puede dar un nombre a cada elemento (en el ejemplo, se ha utilizado x) y se ha modificado. A este tipo de funciones se las denomina closures y se ven en la siguiente sección.

Si se revisa el código anterior paso a paso:

  1. En primer lugar se usa .iter() en el vector1 para obtener unas referencias a los elementos. Después se ha usado map() para sumar 1 y devolver nuevos valores en un nuevo vector. El vector1 aún está disponible ya que solo se han usado referencias a sus elementos: se ha tomado ningún valor. Al final, resulta necesario utilizar collect() para crear un nuevo vector con los nuevos elementos que map() ha ido generando.

  2. En segundo lugar, se usa .into_iter() para obtener un iterador sobre los valores (directamente, sin referencias). Esto destruye vector1. Así que después de construir el nuevo vector1_b, no se puede volver a usar vector1.

  3. Por último, se usa .iter_mut() sobre el vector2. Es mutable, por lo que no se necesita utilizar .collect() para crear un nuevo Vec. En vez de ello, se modifican difectamente los valores del Vec con referencias modificables. Por eso, vector2 sigue existiendo con los valores modificados. Por ello, se utiliza for_each en lugar de map().

Cómo funciona un iterador

Un iterador funciona mediante el uso de un método denominado .next(), que devuelve un Option. Cuando se usa un iterador, Rust llama a la función .next() una y otra vez. Sigue haciéndolo siempre que obtenga un elemento de tipo Some. Se detiene cuando recupera un elemento de tipo None.

Se recordará por un apartado anterior que existe la macro assert_eq!. En la documentación de las librerías de Rust se usa mucho. A continuación, se muestra un ejemplo que la usa para mostrar cómo funciona un iterador:

fn main() {
    let my_vec = vec!['a', 'b', '거', '柳']; // Es un Vector "normal"

    let mut my_vec_iter = my_vec.iter(); // Esta variable contiene un objeto iterador
                                         // que aún no se ha usado

    assert_eq!(my_vec_iter.next(), Some(&'a'));  // Recupera el primer elemento con next()
    assert_eq!(my_vec_iter.next(), Some(&'b'));  // Recupera el siguiente
    assert_eq!(my_vec_iter.next(), Some(&'거')); // Y el siguiente
    assert_eq!(my_vec_iter.next(), Some(&'柳')); // Y el siguiente
    assert_eq!(my_vec_iter.next(), None);        // Ya no queda ninguno, recupera None
    assert_eq!(my_vec_iter.next(), None);        // Si se sigue llamando a .next() se sigue obteniendo None
}

Se puede implementar el rasgo Iterator en cualquier struct o enum sin mucha dificultad. En primer lugar, se crea una biblioteca de libros como base para demostrar cómo se puede hacer.

#[derive(Debug)] // Se quiere usar print con {:?}
struct Library {
    library_type: LibraryType, // Este es el enum
    books: Vec<String>, // lista de libros
}

#[derive(Debug)]
enum LibraryType { // las bibliotecas pueden ser de la ciudad o del país
    City,
    Country,
}

impl Library {
    fn add_book(&mut self, book: &str) { // Se usa add_book para añadir nuevos libros
        self.books.push(book.to_string()); // Se toma un &str y lo convierte en String, para añadirlo después a Vec
    }

    fn new() -> Self { // Esta función crea una nueva biblioteca
        Self {
            library_type: LibraryType::City, // La mayoría son de ciudades, por lo qu ese usa como valor por defecto
                                             // la mayor parte de las veces.
            books: Vec::new(),
        }
    }
}

fn main() {
    let mut my_library = Library::new(); // crea una nueva biblioteca
    my_library.add_book("The Doom of the Darksword"); // se añaden algunos libros
    my_library.add_book("Demian - die Geschichte einer Jugend");
    my_library.add_book("구운몽");
    my_library.add_book("吾輩は猫である");

    println!("{:?}", my_library.books); // se puede imprimir la lista de libros
}

Ahora se puede implementar Iterator para la biblioteca para que se pueda utilizar en un bucle for. Con la implementación anterior, antes de implementar Iterator, el bucle for no funciona.

#![allow(unused)]
fn main() {
for item in my_library {
    println!("{}", item); // ⚠️
}
}

Devuelve lo siguente:

error[E0277]: `Library` is not an iterator
  --> src\main.rs:47:16
   |
47 |    for item in my_library {
   |                ^^^^^^^^^^ `Library` is not an iterator
   |
   = help: the trait `std::iter::Iterator` is not implemented for `Library`
   = note: required by `std::iter::IntoIterator::into_iter`

Pero se puede hacer que la biblioteca implemente un iterador con impl Iterator for Library. La información sobre el rasgo Iterator se encuentra en la documentación de la librería estándar: https://doc.rust-lang.org/std/iter/trait.Iterator.html

En la parte superior izquierda de esta página indica Associated Types: Item y Required Methods: next. Un tipo asociado es otro tipo que va junto con este. En este caso, el tipo asociado será String ya que se quiere que el iterador a implementar devuelva cadenas de texto.

En la página de la documentación hay un ejemplo:

// an iterator which alternates between Some and None
struct Alternate {
    state: i32,
}

impl Iterator for Alternate {
    type Item = i32;

    fn next(&mut self) -> Option<i32> {
        let val = self.state;
        self.state = self.state + 1;

        // if it's even, Some(i32), else None
        if val % 2 == 0 {
            Some(val)
        } else {
            None
        }
    }
}

fn main() {}

Se puede observa que bajo impl Iterator for Alternate dice type Item = i32. Este es el tipo asociado para este ejemplo. En el caso del ejemplo de la Librería, el iterador devolverá libros de la lista que es de tipo Vec<String>. Cuando se llame a .next() devolverá un String de los incluidos en el vector. Por lo tanto, el tipo asociado será type Item = String.

Para implementar Iterator se debe escribir la función fn next(). En ella se indicará lo que debe hacer el iterador. Para este caso, se quiere que entregue primero los últimos libros del vector. En el ejemplo que sigue, se usa match para ver el tipo de elemento que ha devuelvo .pop() y seleccionar qué debe devolver next(). Además, se imprimirá " encontrado" para cada elemento:

#[derive(Debug, Clone)]
struct Library {
    library_type: LibraryType,
    books: Vec<String>,
}

#[derive(Debug, Clone)]
enum LibraryType {
    City,
    Country,
}

impl Library {
    fn add_book(&mut self, book: &str) {
        self.books.push(book.to_string());
    }

    fn new() -> Self {
        Self {
            library_type: LibraryType::City,
            // la mayor parte de las veces, valor por defecto
            books: Vec::new(),
        }
    }
}

impl Iterator for Library {
    type Item = String;

    fn next(&mut self) -> Option<String> {
        match self.books.pop() {
            Some(book) => Some(book + " encontrado"), // Rust permite String + &str
            None => None,
        }
    }
}

fn main() {
    let mut my_library = Library::new();
    my_library.add_book("The Doom of the Darksword");
    my_library.add_book("Demian - die Geschichte einer Jugend");
    my_library.add_book("구운몽");
    my_library.add_book("吾輩は猫である");

    for item in my_library.clone() { // ahora sí funciona el bucle for. Se le pasa un clon de la biblioteca para que no se destruya
        println!("{}", item);
    }
}

Esto imprime:

吾輩は猫であるencontrado
구운몽 encontrado
Demian - die Geschichte einer Jugend encontrado
The Doom of the Darksword encontrado