wandres.dev
ITERADORES · la pereza y su poder

Los consumidores: collect, for, count, find, any y all

Un consumidor es lo que pone en marcha la cadena perezosa y la agota, o la corta en cuanto tiene su respuesta. `collect` materializa en una colección vía `FromIterator`; `for` y `count` recorren hasta el final; `find`, `any` y `all` cortocircuitan y hasta pueden domar iteradores infinitos. La frontera entre consumir del todo y consumir lo justo.

⏱ 18 min

Los adaptadores describen; los consumidores ejecutan. Un consumidor es el eslabón final que empieza a llamar a next y arrastra tras de sí a toda la cadena perezosa hasta obtener un resultado tangible: una colección, un número, un booleano, un elemento encontrado. Pero no todos consumen igual. Unos exigen ver cada elemento —collect, count, sum—; otros se detienen en cuanto tienen su respuesta —find, any, all—, y esa capacidad de parar temprano no es un detalle de eficiencia: es lo que permite consumir con sentido un iterador infinito.

🎯 Al terminar esta lección sabrás
  • Materializar un iterador en una colección con collect y FromIterator.
  • Reconocer for como el consumidor arquetípico y count como el que agota contando.
  • Buscar con find y decidir con any y all, entendiendo el cortocircuito.
  • Distinguir los consumidores que agotan de los que paran temprano.

collect: materializar en una colección

collect es el consumidor estrella: recorre el iterador y vuelca sus valores en una colección. Puede construir un Vec, un String, un HashMap, un HashSet y muchos más, porque delega en el trait FromIterator. Como del mismo iterador podría salir más de una colección, casi siempre debes decir cuál quieres —con anotación o con turbofish—:

let v: Vec<i32> = (1..=5).collect();
let dobles = (1..=5).map(|x| x * 2).collect::<Vec<_>>();

use std::collections::HashMap;
let mapa: HashMap<i32, i32> = (1..=3).map(|n| (n, n * n)).collect();
// {1: 1, 2: 4, 3: 9}

Un truco decisivo: collect sabe reunir una secuencia de Result en un Result de secuencia, cortando en el primer error. Es la forma idiomática de aplicar una operación falible a muchos elementos y quedarte con “todo bien” o “el primer fallo”:

let ok: Result<Vec<i32>, _> = ["1", "2", "3"].iter().map(|s| s.parse::<i32>()).collect();
// Ok([1, 2, 3])
let mal: Result<Vec<i32>, _> = ["1", "x", "3"].iter().map(|s| s.parse::<i32>()).collect();
// Err(...): se detiene en "x", ni siquiera intenta el "3"

for y count: recorrer hasta el final

El bucle for es el consumidor más básico y el más común: pide next en un bucle y liga cada valor. Lo verás desazucarado en la próxima lección; por ahora, reconócelo como un consumidor de pleno derecho. count agota el iterador y devuelve cuántos elementos produjo:

for x in (1..=3).map(|n| n * 10) {
    println!("{x}");                 // 10, 20, 30
}
let cuantos = "abracadabra".chars().filter(|c| *c == 'a').count();  // 5

Ambos son totales: no pueden responder sin haber visto el último elemento. count recorre entero para poder contar; for ejecuta el cuerpo para cada valor. Sobre un iterador infinito, cualquiera de los dos no termina jamás.

find, any y all: consumir con cortocircuito

Aquí cambia la naturaleza del consumo. find devuelve el primer elemento que cumple un predicado, envuelto en Option, y se detiene ahí mismo:

let primero_par = (1..=100).find(|n| n % 2 == 0);   // Some(2), no mira mas alla

any responde “¿hay alguno que cumpla?” y para en el primer true; all responde “¿lo cumplen todos?” y para en el primer false:

let hay_negativo = [1, -3, 5].iter().any(|n| *n < 0);   // true, se detiene en -3
let todos_pos = [1, 2, 3].iter().all(|n| *n > 0);       // true, tuvo que verlos todos

Estos tres cortocircuitan: no prometen recorrer la secuencia entera, solo lo justo para decidir. La consecuencia es notable: pueden consumir un iterador infinito y terminar, siempre que la respuesta llegue en un número finito de pasos:

let existe_cuadrado_grande = (1..).any(|n| n * n > 1_000);  // true, para en n = 32

Un any sobre 1.. —infinito— termina, porque en cuanto encuentra un true deja de pedir valores. Un count sobre 1.. colgaría para siempre. La diferencia no está en la fuente: está en si el consumidor necesita el final o le basta con una prueba.

Más allá: max, position, nth y reduce

El catálogo es largo, pero cae siempre en las mismas dos familias. Entre los totales, max y min recorren todo para devolver el extremo, y last el último elemento. Entre los de cortocircuito, position devuelve el índice del primer acierto, y nth(k) salta al elemento k descartando los anteriores sin materializarlos:

let mayor = [3, 9, 2, 7].iter().max();                  // Some(&9), recorre todo
let donde = [3, 9, 2, 7].iter().position(|n| *n == 2);  // Some(2), corta al hallarlo
let tercero = (0..).nth(3);                             // Some(3), sobre un rango infinito

Mención aparte merece reduce: es el hermano de fold que toma como inicial el primer elemento en vez de uno que tú provees, y por eso devuelve Option —una secuencia vacía no tiene primer elemento del que partir—:

let producto = (1..=5).reduce(|a, b| a * b);   // Some(120)
let nada = (1..1).reduce(|a, b| a * b);        // None: no habia con que empezar
💡
Elige el consumidor por la pregunta, no por costumbre

Si buscas un elemento, find es mejor que filter(...).next(): dice tu intención y corta igual. Si solo necesitas saber si existe, any gana a find(...).is_some(). Si quieres validar una condición sobre todos, all es más claro y más rápido que filtrar al revés y contar. Y para efectos secundarios sin colección, for_each o un for dicen “aquí ejecuto” mejor que un map perezoso que alguien podría olvidar consumir. El consumidor correcto es el que nombra tu pregunta.

flowchart TB
it[Un iterador perezoso] --> tot[Consumidores totales]
it --> corto[Consumidores con cortocircuito]
tot --> lista1[collect count sum y el bucle for]
corto --> lista2[find any all]
lista1 --> agota[Recorren cada elemento hasta el final]
lista2 --> para[Paran al tener la respuesta]
para --> inf[Pueden terminar sobre iteradores infinitos]
agota --> cuelga[Sobre lo infinito no terminan]
style tot fill:#89b4fa,color:#11111b
style corto fill:#fab387,color:#11111b
style inf fill:#a6e3a1,color:#11111b
style cuelga fill:#f38ba8,color:#11111b
El consumidor es la frontera donde el plan perezoso se vuelve trabajo

Toda la maquinaria perezosa de los iteradores —los adaptadores que solo describen, la tubería que fluye de uno en uno— existe suspendida, sin efecto, hasta que un consumidor la toca. El consumidor es el punto de contacto entre el plan y el mundo: el instante en que una descripción de cómputo se convierte en cómputo. Y hay dos morales distintas de consumir, que conviene no confundir. Los consumidores totalescollect, count, sum, for— son honestos hasta el final: no pueden dar una respuesta sin haber mirado el último elemento, porque su respuesta depende de todos. Los consumidores con cortocircuitofind, any, all— encarnan una idea más sutil: la parcialidad controlada. Dicen “consumiré solo hasta tener certeza”, y por eso son los únicos que pueden mirar de frente a una secuencia infinita y volver con una respuesta; any sobre los naturales termina porque la existencia se decide con un solo testigo, mientras que count sobre los naturales no termina porque contar exige un final que no llega. Esa asimetría —lo existencial se decide en lo finito, lo universal sobre lo infinito no— es lógica pura filtrándose en el diseño de una biblioteca. Y hay una simetría final que cierra el nivel: collect se apoya en FromIterator, que es el reflejo exacto de IntoIterator, el trait que estudiarás a continuación. Si into_iter deshace una estructura en un flujo, collect rehace un flujo en una estructura; consumir e iterar son las dos direcciones de un mismo puente. El consumidor no es el final de la cadena por accidente: es el lugar donde la potencialidad se paga en actualidad, del todo o solo lo justo.

📝
Lo esencial de los consumidores

Un consumidor arranca la cadena perezosa y produce un resultado tangible. Los totalescollect, count, sum, for, max— exigen ver el último elemento; los de cortocircuitofind, any, all, position— paran en cuanto deciden, y solo estos toleran iteradores infinitos. collect es el más versátil: materializa en cualquier colección vía FromIterator y sabe plegar una secuencia de Result en un Result de secuencia que corta al primer error.

⚔️ Consume con intención
  1. Usa collect para transformar (1..=5) en un Vec<i32> y luego en un HashSet<i32>, y explica por qué cada uno necesita una anotación de tipo distinta.
  2. Colecta ["10", "20", "no"].iter().map(|s| s.parse::<i32>()) en un Result<Vec<i32>, _> y observa que devuelve Err; razona por qué no intenta parsear nada tras el fallo.
  3. Compara (1..=1000).find(|n| n % 7 == 0) con (1..=1000).filter(|n| n % 7 == 0).next(): ¿dan lo mismo?, ¿cuál expresa mejor la intención?
  4. Ejecuta (2..).any(|n| n > 10) y explica por qué termina pese a que 2.. es infinito; luego razona por qué (2..).count() no terminaría.
  5. Usa all para verificar que todos los elementos de un Vec son positivos, y any para saber si alguno es cero, describiendo en cada caso cuándo se detiene el recorrido.