wandres.dev
ITERADORES · la pereza y su poder

Adaptadores fundamentales: map, filter, fold, sum y zip

Los cinco adaptadores que resuelven la mayoría de las transformaciones: `map` transforma cada elemento, `filter` selecciona algunos, `zip` empareja dos secuencias, y `fold` reduce todo a un valor acumulando —con `sum` como su caso especial más común—. La distinción entre adaptador perezoso y reductor que consume.

⏱ 18 min

Con next y la pereza ya tienes la mecánica; ahora llega el vocabulario. Casi cualquier manipulación de una secuencia se expresa combinando cinco operaciones: map para transformar, filter para seleccionar, zip para emparejar, y fold para reducirlo todo a un único valor, del que sum es apenas el caso más frecuente. Aprenderlas no es memorizar una API: es adquirir una gramática con la que describir cálculos sobre colecciones sin escribir un solo bucle, y descubrir que bajo esa variedad late una sola operación —acumular— de la que las demás son variaciones.

🎯 Al terminar esta lección sabrás
  • Transformar cada elemento con map y seleccionar con filter.
  • Reducir una secuencia a un valor con fold y su acumulador.
  • Reconocer sum y product como casos especiales de fold.
  • Emparejar dos iteradores con zip y saber cuándo se detiene.

map: transformar cada elemento

map aplica una clausura a cada valor y produce un iterador con los resultados. Es la transformación uno a uno: la longitud no cambia, el tipo puede cambiar por completo.

let nombres = ["ada", "alan", "grace"];
let largos: Vec<usize> = nombres.iter().map(|s| s.len()).collect();
// [3, 4, 5]: de &str a usize

La clausura recibe cada Item y devuelve lo que quieras; map se limita a envolver esa promesa, perezosamente, sin ejecutarla hasta el consumo. Cambiar el tipo de salida es lo normal, no la excepción: de un texto a su longitud, de un registro a un campo, de un error a otro.

filter: quedarse con algunos

filter conserva solo los elementos que satisfacen un predicado. La secuencia se acorta; el tipo se mantiene.

let pares: Vec<i32> = (1..=10).filter(|n| n % 2 == 0).collect();
// [2, 4, 6, 8, 10]

Un matiz técnico revelador: el predicado recibe una referencia al elemento, &Self::Item. Por eso arriba n es &i32 y n % 2 funciona por deref automático. La razón es de propiedad: filter solo inspecciona para decidir, no consume, así que presta el valor en lugar de moverlo, y quien pase el filtro sigue su camino intacto.

fold, sum y product: el acumulador universal

fold es la operación de la que descienden casi todas las demás. Recibe un valor inicial y una clausura de dos argumentos —el acumulador y el elemento actual—, y recorre la secuencia entera fusionando cada elemento en el acumulador:

let suma = (1..=5).fold(0, |acc, x| acc + x);        // 15
let producto = (1..=5).fold(1, |acc, x| acc * x);    // 120
let concat = ["a", "b", "c"]
    .iter()
    .fold(String::new(), |mut s, x| { s.push_str(x); s });  // "abc"

Lee la firma mental: partes de init, y por cada elemento sustituyes el acumulador por el resultado de la clausura. Al agotarse la secuencia, el acumulador final es el resultado. fold no es perezoso: es un consumidor que recorre todo y devuelve un valor. Y fíjate en el poder: con solo cambiar el inicial y la clausura, fold suma, multiplica, concatena, cuenta o construye cualquier estructura.

Sumar y multiplicar son tan comunes que tienen consumidores propios, sum y product, más legibles que el fold equivalente:

let total: i32 = (1..=100).sum();          // 5050
let fact: u64 = (1..=10).product();        // 3628800

Necesitan una anotación de tipo (: i32) porque sum es genérico sobre a qué tipo sumar. Conceptualmente, sum() es fold(0, |a, b| a + b) y product() es fold(1, |a, b| a * b): el mismo motor de acumulación, con nombre propio.

zip: caminar dos a la vez

zip entrelaza dos iteradores en uno de pares, avanzando ambos en paralelo y produciendo tuplas (a, b). Se detiene en cuanto el más corto se agota:

let nombres = ["Ada", "Alan", "Grace"];
let edades = [36, 41, 45];
let fichas: Vec<(&str, i32)> = nombres
    .iter()
    .copied()
    .zip(edades.iter().copied())
    .collect();
// [("Ada", 36), ("Alan", 41), ("Grace", 45)]

Como los adaptadores se componen, zip brilla junto a map: emparejar y luego combinar cada par es un patrón diario, por ejemplo para el producto escalar de dos vectores.

let a = [1, 2, 3];
let b = [4, 5, 6];
let punto: i32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
// 1*4 + 2*5 + 3*6 = 32
🔁

map · adaptador

Transforma cada elemento uno a uno. Perezoso: devuelve un iterador. La longitud se conserva, el tipo puede cambiar.

🧹

filter · adaptador

Conserva los que cumplen un predicado que recibe &Item. Perezoso. La longitud se acorta, el tipo se mantiene.

🪢

zip · adaptador

Empareja dos iteradores en tuplas y se detiene con el más corto. Perezoso.

📉

fold · consumidor

Reduce toda la secuencia a un valor acumulando. Ejecuta. sum y product son casos suyos.

flowchart LR
src[Rango 1 a 5] --> mapa[map duplica cada valor]
mapa --> filtro[filter deja los mayores que cuatro]
filtro --> fold[fold acumula en un total]
fold --> res[Un unico valor final]
style src fill:#a6e3a1,color:#11111b
style fold fill:#89b4fa,color:#11111b
style res fill:#cba6f7,color:#11111b
fold es la operación fundamental de la que nacen las demás

Mira fold de cerca, porque estás ante la operación primitiva de todo procesamiento de secuencias. Su forma —parte de un valor inicial y fusiona cada elemento en un acumulador— es lo que la teoría llama un catamorfismo: la manera canónica de colapsar una estructura en un resultado. Y su generalidad no es retórica. sum es fold con inicial 0 y la suma; product, con 1 y la multiplicación; count es fold(0, |c, _| c + 1); last, max y min son fold que van reteniendo un candidato; hasta collect puede escribirse como un fold que empieza en una colección vacía y empuja cada elemento. Bajo una docena de consumidores distintos late el mismo esqueleto: recorrer una vez, llevando un estado que se actualiza con cada valor. Esa unidad es profunda porque revela la anatomía de la iteración misma: iterar es doblar una secuencia sobre un acumulador, y la clausura que le pasas es el álgebra con la que se dobla —qué significa combinar el resultado acumulado con el siguiente elemento—. Cambia el álgebra y cambias el sentido de la operación sin tocar el esqueleto. Por eso dominar fold no es aprender un método más de una lista larga: es entender la operación de la que la lista larga es solo un catálogo de casos con nombre. Cuando ningún consumidor prefabricado encaja con lo que necesitas, casi siempre la respuesta es un fold con la clausura adecuada; y cuando uno sí encaja, conviene saber que por dentro es un fold. La programación funcional entera puede leerse como el arte de elegir el álgebra correcta para doblar una estructura, y aquí, en cinco letras, está su corazón.

📝
Adaptador o consumidor: la distinción que ordena los cinco

De estos cinco, tres son adaptadores perezososmap, filter, zip— y devuelven iteradores que puedes seguir encadenando; dos son reductores que consumenfold y su caso sum— y devuelven un valor final, cerrando la cadena. Esta línea divisoria, que ya viste en la lección de la pereza, es la que gobierna el próximo tema: los consumidores. Un buen olfato para saber si un método “sigue construyendo” o “ya ejecuta” vale más que memorizar la firma de cada uno.

⚔️ Compón la gramática de los iteradores
  1. Con map, convierte ["1", "2", "3"] en un Vec<i32> parseando cada cadena, y encadena un .sum::<i32>() al final.
  2. Con filter, quédate con los múltiplos de 3 en 1..=30 y cuéntalos con .count(). Explica por qué el predicado recibe &i32 y no i32.
  3. Escribe con fold una expresión que calcule el máximo de [3, 7, 2, 9, 4] sin usar el método max; define tú el acumulador inicial.
  4. Usa zip para emparejar ["lun", "mar", "mie"] con [1, 2, 3] y construye un Vec de tuplas.
  5. Reescribe (1..=5).sum::<i32>() como un fold y (1..=5).product::<i32>() como otro, y comprueba con assert_eq! que dan lo mismo.