wandres.dev
ENUMS AVANZADOS · modelar con precisión

Pattern matching completo: la gramática de los patrones

Patrones de tupla, rangos y el operador de coincidencia, cláusulas where, patrones opcionales y las formas if case, guard case y for case fuera del switch.

⏱ 18 min

El switch es solo el escenario más visible. Debajo hay una gramática de patrones completa, con reglas de composición propias, que aparece en if, en guard, en for, en while y en catch. Dominarla cambia la forma del código: la lógica condicional deja de ser una escalera de comprobaciones y pasa a ser una descripción declarativa de la forma que deben tener los datos.

🎯 Al terminar esta lección sabrás
  • Enumerar los tipos de patrón del lenguaje y cómo se anidan entre sí.
  • Usar patrones de tupla y rangos, y entender el operador de coincidencia.
  • Refinar coincidencias con where sin multiplicar la anidación.
  • Aplicar if case, guard case, for case y while case con soltura.

Los patrones son una gramática

Un patrón no es una expresión: es una plantilla estructural que se compara contra un valor y que, de paso, puede enlazar partes de él a nombres nuevos. Los patrones se componen recursivamente, y esa composición es lo que permite describir en una línea una condición que en estilo imperativo ocuparía diez.

enum Evento {
    case toque(x: Int, y: Int)
    case tecla(Character, modificadores: Set<String>)
    case cierre(codigo: Int?)
}

switch evento {
case .toque(0, 0):                          // patron de caso + patrones de expresion
    print("origen")
case .toque(let x, let y) where x == y:     // enlace + refinamiento
    print("diagonal en \(x)")
case .tecla(let c, _):                      // comodin: no me importa
    print("tecla \(c)")
case .cierre(let codigo?):                  // patron opcional: solo si NO es nil
    print("cierre con \(codigo)")
case .cierre(nil):
    print("cierre limpio")
default:
    break
}

Los patrones disponibles son pocos y encajan unos dentro de otros: el comodín, el identificador, el enlace con let o var, el de tupla, el de caso de enum, el opcional, el de conversión de tipo con is o as, y el de expresión. Todo lo demás es composición.

ℹ️
El patrón opcional no es azúcar de sintaxis suelto

Escribir let codigo? equivale a .some(let codigo). Es coherente porque Optional es un enum de dos casos, así que el patrón de caso ya bastaba; la interrogación es solo una forma abreviada del mismo patrón. Esta unificación explica por qué if case, for case y el desempaquetado de opcionales comparten mecánica: nunca hubo dos sistemas, solo uno.

Tuplas, rangos y el operador de coincidencia

Un patrón de tupla compara varios valores a la vez, y ahí es donde el verificador de exhaustividad del compilador demuestra su alcance: analiza el espacio combinado de todos los componentes y solo acepta el switch si la unión de los patrones cubre el producto cartesiano completo.

func cuadrante(_ punto: (Int, Int)) -> String {
    switch punto {
    case (0, 0):            return "origen"
    case (_, 0):            return "eje horizontal"
    case (0, _):            return "eje vertical"
    case let (x, y) where x > 0 && y > 0:  return "primer cuadrante"
    case (..<0, 1...):      return "segundo cuadrante"
    default:                return "tercero o cuarto"
    }
}

Que case (..<0, 1...) funcione se debe al patrón de expresión: cuando el patrón es una expresión cualquiera, Swift lo evalúa mediante el operador ~=, cuya firma es una función que toma el patrón y el valor y devuelve un booleano. Los rangos lo implementan como pertenencia; los valores Equatable, como igualdad. Y como es un operador normal, puedes sobrecargarlo.

struct Prefijo { let texto: String }

func ~= (patron: Prefijo, valor: String) -> Bool {
    valor.hasPrefix(patron.texto)
}

switch ruta {
case Prefijo(texto: "/api/"):    manejarAPI()
case Prefijo(texto: "/static/"): servirFichero()
default:                         cuatroCeroCuatro()
}
flowchart TD
P[Patron] --> C[Comodin: guion bajo]
P --> B[Enlace con let o var]
P --> T[Tupla: compone otros patrones]
P --> E[Caso de enum con payload]
P --> O[Opcional: forma abreviada de some]
P --> X[Conversion de tipo: is o as]
P --> Q[Expresion: delega en el operador de coincidencia]
Q --> R[Rangos, valores igualables, sobrecargas propias]
style P fill:#89b4fa,color:#11111b
style Q fill:#f9e2af,color:#11111b
style R fill:#a6e3a1,color:#11111b

where: refinar sin anidar

La cláusula where añade una condición arbitraria a un patrón que ya ha coincidido y que ya ha enlazado sus nombres. La diferencia con un if dentro del cuerpo del caso es sustancial: si el where falla, el switch sigue probando los casos siguientes, mientras que un if interno ya ha consumido la rama.

switch resultado {
case .exito(let datos) where datos.isEmpty:
    mostrarVacio()
case .exito(let datos):
    mostrar(datos)
case .fallo(let e) where e.esRecuperable:
    reintentar()
case .fallo(let e):
    abortar(e)
}

El orden importa: los casos se prueban de arriba abajo y el primero que coincide gana. Por eso el caso refinado va siempre antes que el general, y por eso el verificador de exhaustividad ignora los where: no puede demostrar que una condición arbitraria se cumpla, así que un switch cuyos casos lleven todos cláusula where seguirá exigiendo un default.

Del control de flujo a la descripción de formas

El salto conceptual del pattern matching no es sintáctico, es epistemológico. Un if anidado describe cómo decides: comprueba esto, si sale bien mira aquello, si además pasa lo otro entonces actúa. Un patrón describe qué forma tienen los datos que te interesan, y deja que el compilador derive el cómo. La diferencia se paga en dos monedas. La primera es que el compilador puede razonar sobre patrones: sabe si cubren todo el espacio, sabe si uno es inalcanzable porque otro anterior lo subsume, sabe qué nombres quedan enlazados y con qué tipo en cada rama, y todo eso lo verifica antes de ejecutar una sola línea. Sobre una escalera de condicionales no puede demostrar nada de eso. La segunda moneda es humana: un patrón se lee como la especificación del caso que trata, de modo que la estructura del código reproduce la estructura del dominio en vez de la estructura de la máquina. Cuando la forma del programa coincide con la forma del problema, las revisiones de código dejan de buscar ramas olvidadas, porque ya no puede haberlas.

Fuera del switch

Los mismos patrones funcionan en cualquier posición que acepte una condición: if case, guard case, for case, while case y las cláusulas catch, que no son más que patrones aplicados al valor lanzado. Es aquí donde el estilo del código cambia de verdad.

// Una sola rama de interes
if case .cierre(let codigo?) = evento, codigo != 0 {
    registrar("cierre anomalo \(codigo)")
}

// Salida temprana con enlace que sobrevive al guard
guard case .toque(let x, let y) = evento else { return }
usar(x, y)

// Filtrar y enlazar en el propio bucle
let opcionales: [Int?] = [1, nil, 3, nil, 5]
for case let n? in opcionales {
    print(n)              // 1, 3, 5: los nil ni entran al cuerpo
}

for case .toque(let x, _) in eventos where x > 100 {
    print("toque lejano en \(x)")
}
🍊

if case

Una sola forma te interesa y el resto da igual. Sustituye a un switch con un único caso y default vacío.

🫐

guard case

Exige la forma y sigue con los nombres enlazados en el mismo ámbito. Aplana la función entera.

🍇

for case

Filtra y desestructura en la cabecera del bucle. Con where añadido, elimina el continue de guardia.

⚔️ Piensa en patrones
  1. Escribe un switch sobre una tupla de dos enums de tres casos y comprueba cuántos patrones necesitas para que el compilador acepte el código sin default.
  2. Sobrecarga el operador ~= para que un patrón de expresión reconozca cadenas que casen con un sufijo dado.
  3. Reescribe una cadena de tres if anidados con opcionales usando un solo guard case y una cláusula where.
  4. Convierte un bucle que hace continue cuando el elemento es nil en un for case let x?.
  5. Explica por qué un switch cuyos casos llevan todos where sigue exigiendo default, y qué implica eso sobre lo que el compilador puede demostrar.