Iteradores falibles: collect a Result y el corto-circuito
El truco más elegante de los iteradores falibles: recolectar un iterador de Result en un Result<Vec<T>, E> que se corto-circuita ante el primer error. Cómo lo hace FromIterator, el caso gemelo de Option, y las alternativas filter_map y partition.
Aquí vive uno de los trucos más celebrados de Rust. Cuando una cadena de iteradores produce valores que pueden fallar —parsear textos, abrir archivos, validar entradas—, cada paso devuelve un Result. Lo que casi siempre quieres no es una lista de resultados, sino un veredicto único: todo salió bien y aquí están los valores, o algo falló y aquí está el error. Rust lo expresa cambiando una sola cosa: el tipo de destino de collect. Un Result<Vec<T>, E> en lugar de un Vec<Result<T, E>>, y con ese cambio de forma llega, gratis, el corto-circuito.
- Recolectar un iterador de
Resulten unResult<Vec<T>, E>con corto-circuito al primer error. - Entender la implementación de
FromIteratorparaResultque lo hace posible. - Aplicar el mismo patrón a
Option, obteniendo unOption<Vec<T>>. - Elegir entre cortar, descartar o separar los fallos con
collect,filter_mapypartition.
El truco: de muchos Result a un solo Result
Imagina que parseas una lista de textos a números. Cada parseo devuelve Result<i32, _>, así que un map produce un iterador de Result. Lo que quieres es: todos los números si todos parsean, o el primer error si alguno falla. Rust lo consigue con un collect cuyo destino es Result<Vec<i32>, _>:
fn parsea(textos: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
textos.iter().map(|s| s.parse::<i32>()).collect()
}
assert_eq!(parsea(&["1", "2", "3"]).unwrap(), [1, 2, 3]);
assert!(parsea(&["1", "x", "3"]).is_err()); // aborta al llegar a la "x"
Es el collect de siempre, pero el tipo de destino cambia radicalmente la semántica: en vez de un Vec<Result<T, E>> con un resultado por elemento, pide un Result<Vec<T>, E> con un veredicto global. Y con ese cambio de forma pide también un comportamiento nuevo.
Cómo funciona el corto-circuito
Result implementa FromIterator: sabe consumir un iterador de Result<T, E> y plegarlo en un solo Result<Vec<T>, E>. La lógica es exactamente la que escribirías a mano: va desenvolviendo cada Ok y acumulándolo; en cuanto encuentra un Err, deja de consumir el iterador y devuelve ese error.
// Semantica conceptual de FromIterator para Result
let mut acc = Vec::new();
for item in iter {
match item {
Ok(v) => acc.push(v),
Err(e) => return Err(e), // corto-circuito: nada mas se procesa
}
}
Ok(acc)
Dos consecuencias importan. La primera: es perezoso de verdad. Como el iterador es de un solo paso, al abortar en el primer Err los elementos posteriores nunca se evalúan; si el map hacía trabajo caro, ese trabajo se ahorra. La segunda: se conserva solo el primer error, no todos. Si necesitas acumular todos los fallos, collect a Result no es la herramienta: lo son las bibliotecas de validación o un partition.
flowchart LR it[Iterador de Result de T y E] --> col[collect a Result de Vec y E] col --> ok[Todos Ok devuelve Ok con el Vec completo] col --> err[Primer Err corta y lo devuelve] style it fill:#cba6f7,color:#11111b style ok fill:#a6e3a1,color:#11111b style err fill:#f38ba8,color:#11111b
Option, y las demás variantes
Option juega el mismo juego: un iterador de Option<T> se recolecta en Option<Vec<T>>, que es Some con todos los valores si ninguno fue None, y None en cuanto aparece el primero.
let todos: Option<Vec<i32>> = ["1", "2"].iter().map(|s| s.parse().ok()).collect();
assert_eq!(todos, Some(vec![1, 2]));
let falla: Option<Vec<i32>> = ["1", "x"].iter().map(|s| s.parse::<i32>().ok()).collect();
assert_eq!(falla, None);
Cuando no quieres cortar sino descartar los fallos y quedarte con los aciertos, el verbo es filter_map, que conserva cada Some y tira cada None:
let validos: Vec<i32> = ["1", "x", "3"].iter().filter_map(|s| s.parse().ok()).collect();
assert_eq!(validos, [1, 3]); // ignora la "x", no aborta
Y si lo que buscas es separar aciertos de errores en dos colecciones, partition reparte según un criterio. Elegir entre cortar, descartar o separar es una decisión de diseño sobre qué significa un fallo en tu dominio.
El puente con el operador ?
El collect falible encaja de forma natural con el resto del manejo de errores. Como devuelve un Result, se compone con el operador ?: una función puede recolectar, propagar el error hacia arriba si lo hay, y seguir trabajando con el Vec ya desenvuelto si no lo hay.
use std::num::ParseIntError;
fn suma_de(textos: &[&str]) -> Result<i32, ParseIntError> {
let numeros: Vec<i32> = textos.iter().map(|s| s.parse()).collect::<Result<_, _>>()?;
Ok(numeros.iter().sum())
}
assert_eq!(suma_de(&["10", "20", "12"]).unwrap(), 42);
assert!(suma_de(&["10", "?", "12"]).is_err()); // el "?" no parsea, corta y propaga
El turbofish collect::<Result<_, _>>() fija que el destino es un Result y deja inferir lo demás; el ? que le sigue desenvuelve el Ok o retorna el Err. Es el idioma canónico para convertir una lista de entradas fallibles en una sola operación fallible, sin un solo match explícito y sin materializar los errores intermedios.
El mismo collect que secuencia Result secuencia Option, y la idea reaparece con sum y product sobre iteradores de Result: iter.map(...).sum::<Result<i32, _>>() suma solo si todos son Ok. Cuando veas un iterador de valores fallibles, pregúntate siempre qué quieres: cortar al primer fallo con collect a Result, descartarlos con filter_map o separarlos con partition.
Lo que ocurre cuando un Vec<Result<T, E>> se convierte en un Result<Vec<T>, E> tiene nombre en la teoría de categorías: es una secuenciación, la operación que intercambia el orden de dos constructores de tipo —de “lista de fallibles” a “fallible de lista”—. No es un adorno académico: es la idea de que la forma de un tipo y su significado son la misma cosa, y que reordenar la forma reordena, con ella, la lógica de control. Un iterador de Result no dice nada sobre qué hacer ante un error; es una mera yuxtaposición de éxitos y fallos posibles. Pero en el instante en que le pides collect hacia Result<Vec<T>, E>, has declarado una política: “esto solo tiene sentido si todo sale bien; al primer tropiezo, abandona y comunica el fallo”. Toda la maquinaria del corto-circuito —parar de consumir, propagar el primer error, no evaluar lo que sobra— no la escribes: la invocas cambiando un tipo. Esto es lo que distingue a Rust de la programación defensiva a base de comprobar un código de error tras cada llamada, ese ruido que satura otros lenguajes: aquí el manejo de errores no es una capa de fontanería que rocías sobre la lógica, sino una propiedad del tipo de destino que el compilador materializa por ti. Y el mismo esqueleto sirve para Option —secuenciar ausencias— y se extiende a cualquier tipo que sepa comportarse como un contexto fallible. Comprender que collect no “recolecta” sino que secuencia efectos es cruzar una frontera conceptual: dejas de ver los iteradores como bucles disfrazados y empiezas a verlos como un álgebra de flujos, donde elegir el tipo de llegada es elegir, de un plumazo, la semántica entera del recorrido.
- Escribe una función que reciba
&[&str]y devuelvaResult<Vec<i32>, _>parseando cada elemento; comprueba que un valor inválido aborta con error. - Demuestra la pereza del corto-circuito: intercala un
inspectque imprima antes del parseo y verifica que los elementos tras el primerErrno se imprimen. - Cambia el destino a
Option<Vec<i32>>con.parse().ok()y contrasta su comportamiento con el deResult. - Sobre la misma entrada mixta, usa
filter_mappara quedarte solo con los válidos, y explica en qué se diferencia decollectaResult. - Investiga
partitionpara separar aciertos y errores en dosVec, y razona cuándo preferirías acumular todos los fallos en vez de cortar en el primero.