Los combinadores: transformar sin desenvolver a mano
map, and_then, ok_or, map_err y unwrap_or: encadenar transformaciones sobre Option y Result sin abrir el enum en cada paso. La vía ferroviaria donde el None o el Err cortocircuitan solos y el camino feliz queda lineal.
Abrir un Option o un Result con match en cada paso funciona, pero cuando encadenas varias operaciones el código se convierte en una escalera de comprobaciones anidadas que entierra la lógica de verdad. Los combinadores son métodos que transforman el valor de dentro sin obligarte a sacarlo: aplican tu función solo cuando hay algo que aplicar, y dejan que el None o el Err se propaguen solos. El resultado es código que se lee como una tubería —una transformación tras otra— donde el camino del fallo desaparece de la vista sin dejar de estar controlado.
- Usar
mappara transformar el interior de unOption/Resultsin abrirlo. - Encadenar operaciones que también pueden fallar con
and_then, y ver por qué nomap. - Convertir entre mundos con
ok_or,oky transformar el error conmap_err. - Cerrar la tubería con
unwrap_ory familia, aportando el valor final.
map: transformar el interior sin abrirlo
map aplica una función al valor si existe, y no hace nada si no. Sobre Option, transforma el Some y deja el None intacto:
let largo: Option<usize> = Some("hola").map(|s| s.len()); // Some(4)
let nada: Option<i32> = None;
let doble = nada.map(|x| x * 2); // None (map ni la toca)
Sobre Result, map transforma el Ok y deja el Err pasar sin cambios:
let r: Result<i32, String> = Ok(21);
let d = r.map(|x| x * 2); // Ok(42)
let e: Result<i32, String> = Err("mal".into());
let f = e.map(|x| x * 2); // Err("mal"): la función ni se ejecuta
La clave conceptual: map preserva la estructura. Un Some sigue siendo Some, un Err sigue siendo Err; solo cambia lo que hay dentro del éxito. Tu función |x| x * 2 no sabe nada de Option ni de Result: opera sobre el valor limpio, y el combinador se encarga del envoltorio.
and_then: encadenar lo que también puede fallar
¿Qué pasa si la función que quieres aplicar también devuelve un Option? Con map acabarías con un Option<Option<T>> anidado, casi nunca lo que quieres:
fn mitad(n: i32) -> Option<i32> {
if n % 2 == 0 { Some(n / 2) } else { None }
}
let anidado: Option<Option<i32>> = Some(8).map(mitad); // Some(Some(4)) — feo
and_then es la solución: aplana. Espera que tu función devuelva ya un Option, y encadena sin duplicar la envoltura. Si algún eslabón da None, toda la cadena da None:
let a = Some(8).and_then(mitad).and_then(mitad); // 8→4→2 = Some(2)
let b = Some(6).and_then(mitad).and_then(mitad); // 6→3→None (3 es impar)
La regla es limpia: map cuando tu función devuelve un valor normal; and_then cuando tu función devuelve otro Option/Result. En términos técnicos, map es el functor y and_then la operación monádica (el flatMap de otros lenguajes), pero no necesitas esa jerga: basta recordar que and_then aplana un nivel que map dejaría anidado.
Puentes entre mundos: ok_or, ok, map_err
Option y Result se convierten el uno en el otro con combinadores dedicados. Para pasar de ausencia a fallo con causa, ok_or adjunta el error que le des al None:
let o: Option<i32> = Some(3);
let r: Result<i32, &str> = o.ok_or("faltaba el valor"); // Ok(3)
let v: Result<i32, &str> = None.ok_or("faltaba el valor"); // Err("faltaba el valor")
// ok_or_else para construir el error de forma perezosa (igual que unwrap_or_else de 7.2)
let r2 = o.ok_or_else(|| format!("no encontrado a las {}", 12));
En sentido inverso, ok descarta el error y te deja solo la presencia o ausencia:
let n: Option<i32> = "42".parse().ok(); // Some(42)
let m: Option<i32> = "abc".parse().ok(); // None (el ParseIntError se tira)
Y map_err transforma el error sin tocar el éxito —imprescindible para adaptar el E de una librería al E de tu función, algo que el operador ? de 7.5 hará casi solo:
let r: Result<i32, String> = "x".parse::<i32>()
.map_err(|e| format!("entrada inválida: {e}"));
flowchart LR A[Valor de entrada] --> B[map transforma el interior] B --> C[and_then encadena otra operacion falible] C --> D[map_err adapta el error] D --> E[unwrap_or aporta el valor final] A -.->|None o Err| X[Cortocircuito] B -.->|None o Err| X C -.->|None o Err| X X --> E style B fill:#a6e3a1,color:#11111b style C fill:#89b4fa,color:#11111b style D fill:#fab387,color:#11111b style X fill:#f38ba8,color:#11111b
Cerrar la tubería
Al final de una cadena de combinadores casi siempre quieres salir del mundo Option/Result y quedarte con un valor concreto. Ahí reaparecen los métodos de 7.2 —unwrap_or, unwrap_or_else, unwrap_or_default— como el eslabón que aterriza el resultado:
fn config_puerto(entrada: Option<&str>) -> u16 {
entrada
.and_then(|s| s.trim().parse::<u16>().ok()) // texto → Option<u16>
.filter(|&p| p >= 1024) // descarta puertos reservados
.unwrap_or(8080) // defecto si algo falló
}
config_puerto(Some("3000")); // 3000
config_puerto(Some("80")); // 8080 (lo filtró: < 1024)
config_puerto(None); // 8080
config_puerto(Some("xyz")); // 8080 (parse falló → None)
Léelo de arriba abajo: cada línea es un paso de la transformación, y ninguna abre el Option a mano. El camino del fallo —entrada ausente, texto no numérico, puerto prohibido— desemboca todo en el mismo unwrap_or(8080), sin un solo if, sin anidamiento, sin perder ni un caso.
Hay una imagen que ilumina todo esto: piensa en una cadena de combinadores como dos vías de tren paralelas. Por la vía de arriba corre el éxito; por la de abajo, el fallo. Cada combinador —map, and_then, filter— es una estación que solo opera sobre la vía del éxito: si el tren viene por abajo (None o Err), pasa de largo por todas las estaciones sin detenerse, hasta el final. A esto se le llama railway-oriented programming, y su virtud es que separa físicamente las dos preocupaciones que en el código imperativo se enredan sin remedio: la lógica del camino feliz queda arriba, lineal y legible, mientras el manejo del fallo se vuelve implícito y automático —el cortocircuito— en lugar de un if err != nil repetido en cada línea. No has eliminado el manejo de errores; lo has factorizado, sacándolo del primer plano para que el algoritmo respire. Esta es la misma idea que hace que el operador ? de 7.5 sea tan potente: ? no es más que este cortocircuito elevado a sintaxis. Y por eso interiorizar los combinadores no es aprender una lista de métodos, sino aprender a pensar en tuberías de transformación sobre valores que podrían no estar —un cambio de mirada que, una vez hecho, reescribe cómo estructuras casi todo tu código.
- Con
map, convierte unOption<&str>en unOption<usize>con la longitud de la cadena. - Escribe
mitady encadena tresand_then; encuentra una entrada que sobreviva las tres y otra que se caiga en la segunda. - Toma un
Option<i32>y conviértelo enResult<i32, String>conok_or_else, construyendo el mensaje solo cuando seaNone. - Reescribe
config_puertocomo unmatchanidado a mano y compara la legibilidad con la versión de combinadores. - Encadena
parse().ok(),mapyfilterpara leer una edad desde texto, doblarla, y aceptar solo si el resultado es menor que 200, conunwrap_or(0)al final.