Los adaptadores y consumidores de iteradores son métodos que transforman una secuencia o fuerzan su evaluación.
En el artículo anterior dijimos que los iteradores son perezosos. Son como una cadena de montaje detenida. Hoy vamos a aprender a:
- Modificar la cadena de montaje (añadir operaciones que transforman o descartan piezas) con adaptadores.
- Encender el interruptor para que la cadena produzca un resultado mediante un consumidor.
Esta forma de programar, encadenando operaciones pequeñas, combina una API fluida con un estilo funcional.
Adaptadores: transformar la secuencia
Los adaptadores son métodos que toman un iterador y devuelven otro iterador diferente con alguna lógica añadida. Recuerda: aún no se ejecuta nada, solo estamos configurando la tubería (pipeline).
map: transformación uno a uno
Toma cada elemento y le aplica un closure para transformarlo en otra cosa.
let numeros = vec![1, 2, 3];
// Crea un iterador que multiplicará por 2 cuando se le pida.
// El tipo del iterador cambia de Iter<i32> a Map<Iter<i32>, Closure>
let dobles = numeros.iter().map(|x| x * 2);filter: selección
Toma cada elemento y decide si pasa o no, basándose en un closure que devuelve bool (predicado).
let numeros = vec![1, 2, 3, 4, 5];
// Ojo: filter recibe una REFERENCIA al elemento que itera.
// Si iter() ya da referencias (&i32), filter recibe (&&i32).
// Por eso usamos **x o desestructuramos con |&x|
let pares = numeros.iter().filter(|&x| x % 2 == 0);Otros adaptadores útiles
take(n): Toma solo los primerosnelementos y descarta el resto.skip(n): Salta los primerosnelementos.zip(otro_iter): Combina dos iteradores en uno solo que devuelve tuplas(a, b).enumerate(): Devuelve tuplas(índice, valor).
let caracteres = vec!['a', 'b', 'c'];
for (i, c) in caracteres.iter().enumerate() {
println!("En la posición {} está la letra {}", i, c);
}Consumidores: materializar el resultado
Para que todo lo anterior sirva de algo, necesitamos un método que consuma el iterador y genere un valor final.
collect: el recolector universal
Es el consumidor más importante. Transforma el iterador en una Colección (como Vec, String, HashMap, etc.).
El problema es que collect es tan genérico que Rust a menudo no sabe qué quieres crear. ¿Quieres un Vec<i32>? ¿Un HashSet<i32>? ¿Una LinkedList?
Por eso, casi siempre necesitamos ayudar al compilador. Tenemos dos formas:
Anotación de tipo en la variable:
let dobles: Vec<i32> = numeros.iter().map(|x| x * 2).collect();Sintaxis turbofish (::<>):
Es un nombre divertido para una sintaxis curiosa. Le decimos a collect el tipo explícito.
let dobles = numeros.iter().map(|x| x * 2).collect::<Vec<i32>>();Otros consumidores comunes
sum(): Suma todos los elementos (debe ser un tipo numérico).find(predicado): Devuelve el primer elemento que cumple la condición (Option<T>). Cortocircuita (deja de iterar en cuanto lo encuentra).any()/all(): Devuelvenboolsi alguno o todos cumplen la condición.
Encadenar operaciones
Ahora todo encaja. Podemos combinar estas piezas para realizar operaciones complejas de forma muy legible.
Ejemplo: Imagina que tenemos una lista de números. Queremos:
- Quedarnos solo con los mayores de 10.
- Multiplicarlos por 3.
- Sumar el resultado total.
Estilo imperativo con un bucle:
let numeros = vec![5, 12, 3, 20, 8];
let mut suma = 0;
for n in &numeros {
if *n > 10 {
let valor = n * 3;
suma += valor;
}
}
println!("Resultado: {}", suma);Estilo funcional con iteradores:
let numeros = vec![5, 12, 3, 20, 8];
let suma: i32 = numeros.iter()
.filter(|&n| *n > 10) // 1. Filtramos
.map(|n| n * 3) // 2. Transformamos
.sum(); // 3. Consumimos
println!("Resultado: {}", suma);El código funcional describe QUÉ queremos hacer, no CÓMO hacerlo paso a paso. Es más fácil de leer, más difícil de equivocar y, gracias a las optimizaciones de Rust, igual de rápido.
Un ejemplo real: Limpiando datos
Supongamos que tenemos una lista de nombres sucios (con espacios extra) y queremos formatearlos.
fn main() {
let nombres = vec![" luis ", "Ana", " pepe", "admin "];
let nombres_limpios: Vec<String> = nombres.iter()
.map(|s| s.trim()) // Quitamos espacios (devuelve &str)
.filter(|s| !s.is_empty()) // Ignoramos vacíos
.map(|s| s.to_uppercase()) // Convertimos a MAYÚSCULAS (crea String)
.collect(); // Guardamos en un vector
println!("{:?}", nombres_limpios);
// ["LUIS", "ANA", "PEPE", "ADMIN"]
}Fíjate en lo limpio que queda. Con un bucle también podríamos resolverlo, pero necesitaríamos más estado mutable y más niveles de control.