Encadenar iteradores en Rust: pipelines de datos sin bucles explícitos

Encadenar iteradores en Rust es la forma idiomática de transformar colecciones de datos sin bucles explícitos. La evaluación es perezosa: ningún elemento se procesa hasta que el consumidor final lo requiere. El resultado es código expresivo y tan eficiente como el bucle equivalente escrito a mano.

Pipeline básico

let nombres = vec!["Ana García", "Bob Smith", "Carlos López", "Diana Cruz"];

let apellidos: Vec<&str> = nombres.iter()
    .map(|nombre| nombre.split_whitespace().last().unwrap())
    .filter(|apellido| apellido.starts_with('G') || apellido.starts_with('C'))
    .collect();

println!("{:?}", apellidos); // ["García", "Cruz"]

flat_map: aplanar en el pipeline

let ficheros = vec![
    "app.log",
    "error.log",
    "debug.log",
];

let lineas_de_error: Vec<String> = ficheros.iter()
    .flat_map(|&fichero| {
        std::fs::read_to_string(fichero)
            .unwrap_or_default()
            .lines()
            .filter(|l| l.contains("ERROR"))
            .map(String::from)
            .collect::<Vec<_>>()
    })
    .collect();

take_while y skip_while

let numeros = vec![1, 3, 5, 6, 7, 8, 9, 10];

// take_while: toma mientras se cumpla la condición
let impares_iniciales: Vec<_> = numeros.iter()
    .take_while(|&&n| n % 2 != 0)
    .collect();
// [1, 3, 5]

// skip_while: salta mientras se cumpla, luego toma todo
let desde_par: Vec<_> = numeros.iter()
    .skip_while(|&&n| n % 2 != 0)
    .collect();
// [6, 7, 8, 9, 10]

peekable: mirar el siguiente sin consumir

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

let mut iter = datos.iter().peekable();
let mut grupos: Vec<Vec<i32>> = Vec::new();

while iter.peek().is_some() {
    let actual = *iter.peek().unwrap();
    let grupo: Vec<i32> = iter
        .by_ref()
        .take_while(|&&x| x == actual)
        .copied()
        .collect();
    grupos.push(grupo);
}

println!("{:?}", grupos);
// [[1], [2, 2], [3, 3, 3], [4]]

chain: concatenar iteradores

let primero  = vec![1, 2, 3];
let segundo  = vec![4, 5, 6];
let tercero  = vec![7, 8, 9];

let todos: Vec<_> = primero.iter()
    .chain(segundo.iter())
    .chain(tercero.iter())
    .copied()
    .collect();

println!("{:?}", todos); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

enumerate: índice + valor

let frutas = vec!["manzana", "pera", "naranja"];

for (i, fruta) in frutas.iter().enumerate() {
    println!("{}: {}", i, fruta);
}
// 0: manzana
// 1: pera
// 2: naranja

Pipeline complejo: procesamiento de registros

struct Registro {
    usuario: String,
    puntos: u32,
    activo: bool,
}

fn top_usuarios(registros: &[Registro], n: usize) -> Vec<&str> {
    let mut activos: Vec<&Registro> = registros.iter()
        .filter(|r| r.activo)
        .collect();

    activos.sort_by(|a, b| b.puntos.cmp(&a.puntos));

    activos.iter()
        .take(n)
        .map(|r| r.usuario.as_str())
        .collect()
}

Evaluación perezosa: no procesa de más

// Solo procesa los primeros 3 elementos
let primeros_cuadrados: Vec<_> = (0..)          // iterador infinito
    .map(|x: u64| x * x)
    .take(3)
    .collect();

println!("{:?}", primeros_cuadrados); // [0, 1, 4]

Resumen

  • Los iteradores son perezosos: no procesan hasta que un consumidor lo requiere.
  • flat_map: aplana una iteración de colecciones en una sola secuencia.
  • take_while / skip_while: corta basándose en una condición.
  • peekable: permite mirar el siguiente elemento sin consumirlo.
  • chain: concatena iteradores sin copiar los datos.
  • enumerate: añade índice a cada elemento.

El siguiente artículo muestra cómo implementar el trait Iterator en tus propios tipos para obtener gratis todos los métodos del ecosistema de iteradores.

COMPARTE ESTE ARTÍCULO

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