Saltar al contenido

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.

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.

  • 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—:

Rust
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”:

Rust
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:

Rust
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:

Rust
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:

Rust
let hay_negativo = [1, -3, 5].iter().any(|n| *n < 0);   // true, se detiene en -3let 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:

Rust
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:

Rust
let mayor = [3, 9, 2, 7].iter().max();                  // Some(&9), recorre todolet donde = [3, 9, 2, 7].iter().position(|n| *n == 2);  // Some(2), corta al hallarlolet 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—:

Rust
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.

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 totales —collect, 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 cortocircuito —find, 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 totales —collect, count, sum, for, max— exigen ver el último elemento; los de cortocircuito —find, 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.