wandres.dev
OPCIONALES A FONDO · el arte de la ausencia

Transformar sin desenvolver: map, flatMap y el encadenamiento

Trabajar dentro del opcional en vez de abrirlo: map como aplicación bajo la caja, flatMap como aplanado de la doble ausencia, el encadenamiento opcional como azúcar de flatMap y el operador de coalescencia como la salida deliberada del contexto.

⏱ 17 min

Desenvolver es abrir la caja, hacer algo y volver a cerrarla. Cuando lo único que quieres es transformar el contenido, esa apertura es ruido: introduce una rama que no aporta ninguna decisión, porque el caso vacío se limita a propagarse. Swift ofrece la alternativa: operar dentro del opcional y desenvolver una sola vez, al final, cuando el valor por fin tiene que salir al mundo no opcional.

🎯 Al terminar esta lección sabrás
  • Usar map para aplicar una función al contenido sin abrir la caja.
  • Reconocer cuándo hace falta flatMap y qué doble ausencia aplana.
  • Leer el encadenamiento opcional como el mismo mecanismo con otra sintaxis.
  • Colocar el operador ?? al final de la tubería y no en cada paso.

map: aplicar una función bajo la caja

Optional.map toma una función del tipo envuelto a otro tipo y devuelve un opcional del segundo. Si había valor, lo transforma; si no, propaga la ausencia. Su implementación cabe en cinco líneas y no esconde nada:

extension Optional {
    func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U? {
        switch self {
        case .some(let x): return .some(try transform(x))
        case .none:        return .none
        }
    }
}

Compara las dos versiones del mismo cálculo. La primera abre la caja para nada; la segunda declara la intención:

let apodo: String? = leerApodo()

// abriendo: una rama que no decide nada
var inicial1: Character?
if let apodo { inicial1 = apodo.first } else { inicial1 = nil }

// dentro: la ausencia se propaga sola
let inicial2 = apodo.map { $0.uppercased() }?.first

Para acceder a un miembro tienes las dos notaciones y son intercambiables: apodo?.uppercased() y apodo.map { $0.uppercased() } producen el mismo String?. La primera es más breve con miembros; la segunda admite cualquier función libre, incluida una inicializadora pasada por su nombre, como en numero.map(String.init).

Este map es el mismo concepto que el de Array, no una coincidencia de nombre: ambos aplican una función dentro de un contexto preservando la forma del contexto. Un array de tres elementos sigue teniendo tres; un opcional presente sigue presente y uno ausente sigue ausente. Y cumple las dos leyes que se le exigen a esa familia de operaciones: mapear la identidad no cambia nada, y mapear dos funciones seguidas es igual que mapear su composición. Eso es lo que te autoriza a reordenar y fusionar pasos sin cambiar el resultado.

flatMap: cuando la transformación también puede fallar

El problema aparece en cuanto la función que aplicas devuelve, a su vez, un opcional. map respeta la forma, así que envuelve otra vez y te quedas con dos capas:

let entrada: String? = "42"

let dosCapas = entrada.map { Int($0) }        // Int?? : el Int? entra en otro Optional
let unaCapa  = entrada.flatMap { Int($0) }    // Int?  : aplanado

flatMap hace lo mismo que map y después colapsa el resultado: some(some(x)) pasa a ser some(x), y cualquier none de cualquier nivel pasa a ser none. Esa combinación de envolver y aplanar es lo que convierte al opcional en algo componible: puedes encadenar tantos pasos falibles como quieras y seguir teniendo un solo nivel de ausencia al final.

struct Usuario { let planID: String? }

func plan(de usuario: Usuario?) -> Plan? {
    usuario
        .flatMap { $0.planID }        // Usuario? a String?
        .flatMap { catalogo[$0] }     // String? a Plan?
}

flatMap resuelve la composición en serie, pero no la combinación en paralelo: dos opcionales independientes que solo sirven juntos. Para eso basta una función de cuatro líneas que muchos proyectos acaban escribiendo con el nombre de zip, y que devuelve la tupla solo si ambos están presentes:

func ambos<A, B>(_ a: A?, _ b: B?) -> (A, B)? {
    guard let a, let b else { return nil }
    return (a, b)
}

let intervalo = ambos(inicio, fin).map(Intervalo.init)
⚠️
El flatMap de Optional y el de Sequence no son el mismo

Sequence.flatMap concatena colecciones; Optional.flatMap aplana ausencias. Se llaman igual porque abstraen la misma idea, pero operan sobre contextos distintos y confundirlos produce errores de tipos difíciles de leer. El caso mixto —una secuencia de opcionales— tiene su propio nombre desde hace años, compactMap, precisamente para acabar con esa ambigüedad.

El encadenamiento opcional es flatMap con otra cara

Cuando escribes usuario?.correo?.dominio, no estás usando un mecanismo nuevo: cada ?. es exactamente un flatMap cuya función accede a un miembro. Por eso el resultado tiene una sola capa de opcional por mucho que encadenes, y por eso la cadena se corta en el primer eslabón vacío sin evaluar el resto.

let dominio = usuario?.correo?.dominio        // String?
let mismo   = usuario.flatMap { $0.correo }.flatMap { $0.dominio }

Dos detalles del encadenamiento que se usan poco y valen mucho. El primero: cuando el miembro encadenado es un método que no devuelve nada, la cadena produce un Void?, y compararlo con nil te dice si la llamada llegó a ocurrir. El segundo: puedes escribir a través de la cadena, y la escritura simplemente no sucede si algún eslabón falta.

if carrito?.vaciar() != nil {
    print("el carrito existía y se vació")
}

inventario["manzanas"]?.append(contentsOf: nuevas)   // no hace nada si no hay clave
flowchart LR
A[Optional de A] -->|map| B[Optional de B]
A -->|flatMap| C[Optional de C sin doble capa]
C -->|encadenamiento con interrogacion| D[Optional de D]
D -->|operador de coalescencia| E[Valor no opcional listo para usar]
style A fill:#cba6f7,color:#11111b
style C fill:#89b4fa,color:#11111b
style E fill:#a6e3a1,color:#11111b

El operador ?? y el momento de salir del contexto

?? es la puerta de salida: convierte un opcional en un valor concreto aportando un sustituto. Su segundo parámetro es un @autoclosure, así que el valor por defecto solo se evalúa cuando de verdad hace falta, y eso lo hace seguro incluso si es una operación cara.

let nombre = perfil?.nombre ?? cargarNombreDelDisco()   // solo lee disco si falta
let puerto = configurado ?? heredado ?? 8080            // asociativo por la derecha

La técnica está en cuándo usarlo. Un ?? colocado pronto tapa la ausencia antes de que la tubería termine, y a partir de ahí ya no puedes distinguir “no había dato” de “el dato valía el sustituto”. Colocado al final, la ausencia viaja intacta por toda la transformación y solo se resuelve en el borde, donde alguien tiene que enseñar algo en pantalla.

// pronto: el cero contamina el resto del cálculo y ya no se puede auditar
let media1 = (totales ?? 0) / max(cuenta ?? 1, 1)

// tarde: la ausencia se propaga y se decide una sola vez, al presentar
let media2 = totales.map { $0 / cuenta }        // Int?
let texto  = media2.map(String.init) ?? "sin datos"
💡
Cuenta los desenvolvimientos por función

Una heurística que funciona: si una función tiene más de un punto donde abre opcionales, probablemente esté mezclando transformación con decisión. Extrae la transformación a una tubería de map y flatMap que devuelva un opcional, y deja la decisión —guard, switch o ??— en un único sitio, arriba o al final. El código resultante suele perder la mitad de sus ramas.

El opcional no es un obstáculo entre tú y el valor: es un contexto en el que se calcula

Este es el salto conceptual del nivel. Mientras veas el opcional como una caja molesta que hay que abrir para trabajar, escribirás código con una rama por cada dato que pueda faltar, y esas ramas se multiplicarán entre sí hasta que la función sea un árbol imposible de leer. En cuanto lo ves como un contexto de cálculo —un lugar donde las operaciones se aplican y la ausencia se propaga sola— el código cambia de forma: describes la transformación una sola vez y la maquinaria se encarga de que el vacío atraviese la tubería sin que tú escribas una comprobación. Es la misma idea que hace útiles a los arrays, a los resultados y a las tareas asíncronas: en todos, map significa “haz esto dentro” y flatMap significa “haz esto dentro y no me dejes capas de más”. Por eso quien domina esta lección no aprende cuatro métodos, sino un patrón que reaparecerá en cada tipo genérico serio del ecosistema. Y hay una consecuencia de diseño que va más allá del estilo: al no desenvolver, no te ves obligado a inventar un valor de relleno en cada paso, y esos rellenos —el cero, la cadena vacía, la fecha de 1970— son los que borran la diferencia entre “no hay dato” y “el dato es este”, que es exactamente la información que el opcional existía para conservar.

⚔️ Convierte ramas en tuberías
  1. Busca una función tuya con dos o más if let que solo transformen, y reescríbela como una cadena de map y flatMap que devuelva un opcional.
  2. Aplica map con una función que devuelva un opcional, observa el tipo de doble capa que sale y arréglalo con flatMap.
  3. Traduce una cadena a?.b?.c a flatMap explícitos y comprueba que ambas versiones tienen el mismo tipo.
  4. Usa el Void? de una llamada encadenada para registrar si la operación llegó a ejecutarse.
  5. Coge un cálculo tuyo con varios ?? 0 intermedios, mueve el único ?? necesario al final y razona qué información has recuperado por el camino.