Iteradores avanzados en Rust: implementar Iterator, fold, scan, flatten y collect tipado

El trait Iterator de Rust es uno de los más potentes de la biblioteca estándar. Implementándolo en tus propios tipos obtienes acceso gratuito a más de setenta métodos adaptadores. Más allá del uso básico con map y filter, la librería estándar ofrece combinadores avanzados como fold, scan, flatten, flat_map, chain, zip y peekable que permiten expresar transformaciones complejas de forma concisa y eficiente.

Implementar Iterator desde cero

Cualquier tipo puede convertirse en iterador implementando el trait Iterator: solo es obligatorio definir el tipo asociado Item y el método next(). El resto de métodos tiene implementaciones por defecto construidas sobre next().

struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn nuevo() -> Self {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        let siguiente = self.a + self.b;
        self.a = self.b;
        self.b = siguiente;
        Some(self.a) // infinito: siempre devuelve Some
    }
}

fn main() {
    let primeros_diez: Vec<u64> = Fibonacci::nuevo().take(10).collect();
    println!("{:?}", primeros_diez);
    // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

    let suma_menores_100: u64 = Fibonacci::nuevo()
        .take_while(|&n| n < 100)
        .sum();
    println!("Suma Fibonacci < 100: {suma_menores_100}"); // 142
}

fold y scan: acumuladores con y sin estado visible

fold reduce un iterador a un único valor acumulando con una función binaria. scan hace lo mismo pero produce cada valor intermedio del acumulador, lo que permite inspeccionar el estado en cada paso.

fn main() {
    let ventas = vec![120.0f64, 85.0, 200.0, 50.0, 175.0];

    // fold: suma total con descuento acumulado
    let total = ventas.iter().fold(0.0, |acc, &v| acc + v * 0.9);
    println!("Total con 10% dto: {:.2}", total); // 567.00

    // scan: saldo acumulado día a día
    let saldo_diario: Vec<f64> = ventas.iter()
        .scan(0.0f64, |acum, &v| {
            *acum += v;
            Some(*acum)
        })
        .collect();
    println!("Saldos: {:?}", saldo_diario);
    // [120.0, 205.0, 405.0, 455.0, 630.0]
}

flatten y flat_map: aplanar iteradores anidados

flatten convierte un iterador de iteradores en un iterador plano. flat_map es la combinación de map seguido de flatten: aplica una función que devuelve un iterador y lo aplana automáticamente.

fn main() {
    // flatten con Vec de Vec
    let matriz = vec![vec![1, 2, 3], vec![4, 5], vec![6, 7, 8, 9]];
    let plano: Vec<i32> = matriz.into_iter().flatten().collect();
    println!("{:?}", plano); // [1,2,3,4,5,6,7,8,9]

    // flat_map: dividir frases en palabras
    let frases = vec!["hola mundo", "rust es genial"];
    let palabras: Vec<&str> = frases.iter()
        .flat_map(|frase| frase.split_whitespace())
        .collect();
    println!("{:?}", palabras); // ["hola","mundo","rust","es","genial"]

    // Generar rangos con flat_map
    let expandido: Vec<u32> = (1..=4)
        .flat_map(|n| (0..n))
        .collect();
    println!("{:?}", expandido); // [0, 0,1, 0,1,2, 0,1,2,3]
}

chain, zip y peekable

chain encadena dos iteradores del mismo tipo de ítem en secuencia. zip combina dos iteradores produciendo pares hasta que uno se agote. peekable permite inspeccionar el siguiente elemento sin consumirlo.

fn main() {
    // chain: combinar dos secuencias
    let a = vec![1, 2, 3];
    let b = vec![4, 5, 6];
    let combinado: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
    println!("{:?}", combinado); // [1,2,3,4,5,6]

    // zip: parear temperaturas con ciudades
    let ciudades = ["Madrid", "Barcelona", "Valencia"];
    let temps = [25.0f32, 28.0, 30.0];
    let pares: Vec<_> = ciudades.iter().zip(temps.iter()).collect();
    for (ciudad, temp) in &pares {
        println!("{ciudad}: {temp}°C");
    }

    // peekable: lookahead sin consumir
    let numeros = vec![1, 2, 3, 5, 8, 13];
    let mut iter = numeros.iter().peekable();
    while let Some(&actual) = iter.next().as_ref() {
        if let Some(&&siguiente) = iter.peek() {
            println!("{actual} ? {siguiente} (ratio: {:.2})", siguiente as f64 / actual as f64);
        }
    }
}

collect tipado en distintas colecciones

collect puede producir distintos tipos de colección según la anotación de tipo. Admite Vec, HashMap, BTreeSet, String y cualquier tipo que implemente FromIterator.

use std::collections::{HashMap, BTreeSet};

fn main() {
    let datos = vec!["rust", "python", "rust", "go", "python", "rust"];

    // Contar frecuencias con HashMap
    let frecuencias: HashMap<&str, usize> = datos.iter()
        .fold(HashMap::new(), |mut map, &lang| {
            *map.entry(lang).or_insert(0) += 1;
            map
        });
    println!("{:?}", frecuencias); // {"rust": 3, "python": 2, "go": 1}

    // Deduplicar y ordenar con BTreeSet
    let unicos: BTreeSet<&&str> = datos.iter().collect();
    println!("{:?}", unicos); // {"go", "python", "rust"}

    // Construir String desde chars filtrados
    let texto = "h3ll0 w0rld!";
    let solo_letras: String = texto.chars()
        .filter(|c| c.is_alphabetic())
        .collect();
    println!("{solo_letras}"); // hllwrld

    // Result<Vec, E>: collect que propaga errores
    let cadenas = vec!["1", "2", "3", "4"];
    let numeros: Result<Vec<i32>, _> = cadenas.iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("{:?}", numeros); // Ok([1, 2, 3, 4])
}

El patrón Result<Vec<T>, E> en collect es especialmente útil: si alguna transformación falla, el collect devuelve el primer Err inmediatamente, cortocircuitando el iterador. Es el equivalente idiomático de un bucle con ?.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP