Iteradores en Rust: map, filter, collect y por qué son zero-cost abstractions

Los iteradores de Rust son zero-cost abstractions: escribes map, filter y collect encadenados y el compilador genera código tan eficiente como si hubieras escrito un bucle for a mano. No hay objetos intermedios, no hay asignaciones de memoria innecesarias. Solo código máquina optimizado.

El trait Iterator

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ...más de 70 métodos con implementaciones por defecto
}

Solo necesitas implementar next(). Los demás métodos (map, filter, collect, fold…) vienen gratis.

iter(), iter_mut() e into_iter()

let v = vec![1, 2, 3];

// iter(): presta cada elemento (&T)
for x in v.iter() {
    println!("{}", x); // x es &i32
}

// iter_mut(): presta mutablamente (&mut T)
let mut v2 = vec![1, 2, 3];
for x in v2.iter_mut() {
    *x *= 2; // x es &mut i32
}

// into_iter(): consume la colección (T)
for x in v.into_iter() {
    println!("{}", x); // x es i32, v ya no existe
}

Adaptadores: transforman el iterador

let v = vec![1, 2, 3, 4, 5, 6];

// map: aplica una función a cada elemento
let cuadrados: Vec<_> = v.iter().map(|&x| x * x).collect();

// filter: conserva solo los que cumplen la condición
let pares: Vec<_> = v.iter().filter(|&&x| x % 2 == 0).collect();

// flat_map: aplana el resultado
let palabras = vec!["hola mundo", "foo bar"];
let letras: Vec<&str> = palabras.iter()
    .flat_map(|s| s.split_whitespace())
    .collect();
// ["hola", "mundo", "foo", "bar"]

// zip: combina dos iteradores en pares
let a = vec![1, 2, 3];
let b = vec!["uno", "dos", "tres"];
let pares: Vec<_> = a.iter().zip(b.iter()).collect();
// [(1, "uno"), (2, "dos"), (3, "tres")]

Consumidores: producen un valor final

let v = vec![1, 2, 3, 4, 5];

// collect: recoge en una colección
let v2: Vec<i32> = v.iter().map(|&x| x * 2).collect();

// sum / product
let suma: i32 = v.iter().sum();
let producto: i32 = v.iter().product();

// fold: reduce con acumulador
let suma_manual = v.iter().fold(0, |acc, &x| acc + x);

// count: número de elementos
let n = v.iter().filter(|&&x| x > 2).count(); // 3

// any / all
let alguno_par = v.iter().any(|&x| x % 2 == 0);   // true
let todos_positivos = v.iter().all(|&x| x > 0);    // true

// find
let primero_par = v.iter().find(|&&x| x % 2 == 0); // Some(2)

// max / min
println!("{:?}", v.iter().max()); // Some(5)

Por qué son zero-cost

Los adaptadores son lazy: no hacen nada hasta que un consumidor los activa. Rust fusiona toda la cadena en un único bucle en tiempo de compilación.

// Este código genera el mismo assembly que el bucle manual
let resultado: i32 = (0..1_000_000)
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .take(100)
    .sum();

Depurar con inspect

let resultado: Vec<_> = vec![1, 2, 3, 4, 5]
    .iter()
    .inspect(|x| println!("antes filter: {}", x))
    .filter(|&&x| x % 2 == 0)
    .inspect(|x| println!("después filter: {}", x))
    .collect();

Resumen

  • iter(): referencias; iter_mut(): referencias mutables; into_iter(): consume la colección.
  • Adaptadores: map, filter, flat_map, zip, take, skip… (lazy).
  • Consumidores: collect, sum, fold, count, any, all, find… (activan la cadena).
  • Zero-cost: el compilador fusiona la cadena en un único bucle sin overhead.
  • inspect: útil para depurar pipelines de iteradores.

El siguiente artículo profundiza en cómo encadenar iteradores para construir pipelines de transformación complejos y explora adaptadores avanzados como flat_map, take_while y peekable.

COMPARTE ESTE ARTÍCULO

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