wandres.dev
OPTION Y RESULT · sin null ni excepciones

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Usar map para transformar el interior de un Option/Result sin abrirlo.
  • Encadenar operaciones que también pueden fallar con and_then, y ver por qué no map.
  • Convertir entre mundos con ok_or, ok y transformar el error con map_err.
  • Cerrar la tubería con unwrap_or y 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.

Los combinadores son vías paralelas: el error viaja solo por la suya

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.

⚔️ Piensa en tuberías, no en escaleras
  1. Con map, convierte un Option<&str> en un Option<usize> con la longitud de la cadena.
  2. Escribe mitad y encadena tres and_then; encuentra una entrada que sobreviva las tres y otra que se caiga en la segunda.
  3. Toma un Option<i32> y conviértelo en Result<i32, String> con ok_or_else, construyendo el mensaje solo cuando sea None.
  4. Reescribe config_puerto como un match anidado a mano y compara la legibilidad con la versión de combinadores.
  5. Encadena parse().ok(), map y filter para leer una edad desde texto, doblarla, y aceptar solo si el resultado es menor que 200, con unwrap_or(0) al final.