Aprende Rust

Lección 22 de 26 Rust funcional

Iteradores

  • 5 min de lectura
  • 5 ejercicios
  1. Un iterador entrega los valores uno a uno. v.iter() va poniendo los números de v en la cinta.
  2. filter es un adaptador: deja pasar solo los que cumplen la condición. Aquí, los pares: 2, 4 y 6.
  3. map transforma cada valor con una closure: n * 10 convierte el 2, el 4 y el 6 en 20, 40 y 60.
  4. sum es un consumidor: pone en marcha la cadena y la resume en 120. Sin él, el iterador no hace nada.
1/4

Un iterador produce una secuencia de valores, uno detrás de otro. Ya los has usado sin darte cuenta: cada for de Rust funciona con un iterador por debajo.

Lo interesante es todo lo que puedes hacer con ellos: transformar, filtrar, combinar y resumir colecciones encadenando métodos, sin índices ni variables auxiliares. Junto con las closures de la lección anterior, son la forma más idiomática de trabajar con datos en Rust.

El trait Iterator

Un iterador es cualquier tipo que implemente el trait Iterator. Solo exige un método, next, que devuelve el siguiente elemento envuelto en Some, o None cuando ya no quedan:

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // ...y decenas de métodos que ya vienen implementados
}

type Item es el tipo de los elementos que produce. Puedes llamar a next a mano para verlo en acción:

fn main() {
    let numeros = vec![10, 20, 30];
    let mut it = numeros.iter();

    println!("{:?}", it.next());
    println!("{:?}", it.next());
    println!("{:?}", it.next());
    println!("{:?}", it.next());
}

El iterador es mut porque next cambia su estado interno: recuerda por dónde va.

iter, iter_mut e into_iter

Una colección se puede recorrer de tres maneras, igual que se puede usar un valor de tres maneras:

MétodoProduceLa colección…
iter()&TSe presta para leer
iter_mut()&mut TSe presta para modificar
into_iter()TSe consume: ya no podrás usarla
fn main() {
    let mut notas = vec![5, 7, 9];

    for nota in notas.iter_mut() {
        *nota += 1;
    }
    println!("{notas:?}");

    let total: i32 = notas.into_iter().sum();
    println!("Total: {total}");
    // notas ya no existe: into_iter se la ha quedado
}

El for hace esto por ti: for x in &v usa v.iter(), for x in &mut v usa v.iter_mut() y for x in v usa v.into_iter().

Los iteradores son perezosos

Crear un iterador o encadenarle transformaciones no hace nada hasta que algo lo consume:

fn main() {
    let numeros = vec![1, 2, 3];
    let dobles = numeros.iter().map(|x| {
        println!("procesando {x}");
        x * 2
    });

    println!("Todavía no se ha procesado nada");
    let resultado: Vec<i32> = dobles.collect();
    println!("{resultado:?}");
}

Si escribes un map y no lo consumes, el compilador te avisa: iterators are lazy and do nothing unless consumed.

Adaptadores: transformar el iterador

Los adaptadores reciben un iterador y devuelven otro. Se pueden encadenar tantos como quieras:

AdaptadorQué hace
map(f)Transforma cada elemento con la closure f
filter(f)Deja pasar solo los elementos para los que f devuelve true
enumerate()Añade el índice: produce (0, a), (1, b)…
zip(otro)Empareja con otro iterador: (a1, b1), (a2, b2)…
rev()Recorre al revés
take(n) / skip(n)Se queda con los n primeros / se los salta
chain(otro)Pone otro iterador a continuación
fn main() {
    let numeros = [1, 2, 3, 4, 5, 6];

    let cuadrados: Vec<i32> = numeros.iter().map(|n| n * n).collect();
    let pares: Vec<i32> = numeros.into_iter().filter(|n| n % 2 == 0).collect();
    let ultimos: Vec<i32> = numeros.into_iter().rev().take(2).collect();
    let resto: Vec<i32> = numeros.into_iter().skip(4).collect();

    println!("{cuadrados:?}");
    println!("{pares:?}");
    println!("{ultimos:?}");
    println!("{resto:?}");
}

Referencias: El adaptador filter le pasa a la closure una referencia al elemento, para no quitárselo. Por eso a veces verás *n o patrones como |&n|. Si el compilador se queja de tipos como &&i32, es por esto.

enumerate, zip y chain sirven para combinar:

fn main() {
    let nombres = ["Ana", "Luis", "Marta"];
    let edades = [28, 35, 22];

    for (i, nombre) in nombres.iter().enumerate() {
        println!("{i}: {nombre}");
    }

    for (nombre, edad) in nombres.iter().zip(edades.iter()) {
        println!("{nombre} tiene {edad} años");
    }

    let todos: Vec<i32> = [1, 2].into_iter().chain([3, 4]).collect();
    println!("{todos:?}");
}

Consumidores: obtener un resultado

Los consumidores recorren el iterador y producen un valor final. Son los que ponen en marcha toda la cadena:

fn main() {
    let numeros = vec![4, 8, 15, 16, 23, 42];

    let suma: i32 = numeros.iter().sum();
    let mayores = numeros.iter().filter(|n| **n > 10).count();
    let maximo = numeros.iter().max();
    let hay_impares = numeros.iter().any(|n| n % 2 == 1);
    let todos_positivos = numeros.iter().all(|n| *n > 0);
    let primero_grande = numeros.iter().find(|n| **n > 20);
    let posicion = numeros.iter().position(|n| *n == 16);

    println!("suma = {suma}, mayores de 10 = {mayores}");
    println!("máximo = {maximo:?}");
    println!("¿impares? {hay_impares}, ¿positivos? {todos_positivos}");
    println!("primero > 20 = {primero_grande:?}, posición del 16 = {posicion:?}");
}

fold es el consumidor más general: parte de un valor inicial y lo va actualizando con cada elemento. sum o count son casos particulares de fold:

fn main() {
    let digitos = [1, 9, 8, 4];
    let numero = digitos.iter().fold(0, |acumulado, d| acumulado * 10 + d);
    println!("{numero}");
}

collect: de vuelta a una colección

collect construye una colección a partir del iterador. Como puede crear muchas distintas, tienes que indicar cuál con el tipo de la variable:

use std::collections::HashMap;

fn main() {
    let palabra: String = ['h', 'o', 'l', 'a'].iter().collect();
    let gritando: String = "rust".chars().map(|c| c.to_ascii_uppercase()).collect();
    let precios: HashMap<&str, u32> = [("pan", 1), ("leche", 2)].into_iter().collect();

    println!("{palabra} {gritando}");
    println!("El pan cuesta {} €", precios["pan"]);
}

Para un HashMap basta con que el iterador produzca tuplas (clave, valor).

Encadenar todo

La potencia de verdad aparece al combinar adaptadores y consumidores en una sola cadena. Así se procesa v.iter().filter(|n| *n % 2 == 0).map(|n| n * 10).sum():

Cada elemento recorre la cadena entera antes de que empiece el siguiente, así que no se crean vectores intermedios. Un ejemplo más realista:

fn main() {
    let productos = vec![("teclado", 45), ("monitor", 180), ("cable", 5), ("silla", 120)];

    let caros: Vec<String> = productos
        .iter()
        .filter(|(_, precio)| *precio >= 50)
        .map(|(nombre, precio)| format!("{nombre}: {precio} €"))
        .collect();

    let total: i32 = productos.iter().map(|(_, precio)| precio).sum();

    println!("{caros:?}");
    println!("Total: {total} €");
}

Compáralo con la versión con bucle: hace lo mismo, pero hay que leerla entera para saber qué hace.

let mut caros = Vec::new();
for (nombre, precio) in &productos {
    if *precio >= 50 {
        caros.push(format!("{nombre}: {precio} €"));
    }
}

Coste cero: Los iteradores son una abstracción de coste cero. El compilador los convierte en el mismo código máquina que el bucle escrito a mano, y a veces en uno más rápido porque puede ahorrarse comprobaciones de límites. Elige la versión que se lea mejor.

Tu propio iterador

Para que un tipo tuyo sea un iterador, implementa Iterator con su next. A cambio, recibes gratis todos los adaptadores y consumidores:

struct Contador {
    actual: u32,
    limite: u32,
}

impl Contador {
    fn new(limite: u32) -> Self {
        Contador { actual: 0, limite }
    }
}

impl Iterator for Contador {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.actual < self.limite {
            self.actual += 1;
            Some(self.actual)
        } else {
            None
        }
    }
}

fn main() {
    let total: u32 = Contador::new(5).sum();
    println!("Suma de 1 a 5: {total}");

    let pares: Vec<u32> = Contador::new(10).filter(|n| n % 2 == 0).collect();
    println!("{pares:?}");
}

Hora de practicar

Ejercicios

0/5 resueltos

Reto 1 de 5

Este código calcula los cuadrados con un bucle. Sustitúyelo por una cadena de iteradores: iter(), map y collect. No uses for.
Salida esperada
[1, 4, 9, 16, 25]
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos