wandres.dev
CUSTOM TYPES · uniones y pattern matching

Patrones anidados y destructuring: leer la forma de los datos

Un patrón no es una condición sino una forma: se escribe igual que se escribiría el valor que se quiere casar, y por eso deconstruir un dato en Elm es literalmente el reflejo especular de haberlo construido. Esta lección lleva el pattern matching hasta sus formas compuestas. Desarrolla el anidamiento —casar un constructor dentro de otro, como Just con un Ok dentro, o descomponer una lista con el operador de cons— y el matching sobre tuplas, que convierte una cascada de condicionales cruzados en una tabla de decisión plana y verificada por exhaustividad. Continúa con el destructuring fuera de case: en los argumentos de una función, en un let, sobre records por nombre de campo, y con el alias as que permite nombrar a la vez el todo y sus partes. Cierra explicando que constructor y patrón son las dos direcciones de un mismo lenguaje, que esa simetría hace que el acceso a un dato compuesto sea total en lugar de parcial, y por qué el matching sobre tuplas es una herramienta de razonamiento y no solo de sintaxis.

⏱ 18 min

Hasta ahora los patrones han sido de un solo nivel: un constructor y sus datos ligados a nombres. Pero los datos reales llegan encajados unos dentro de otros —un resultado dentro de una opción, una lista dentro de un caso de éxito, dos estados que hay que considerar a la vez— y desmontarlos capa por capa, con un case dentro de otro case dentro de un tercero, produce el código escalonado que todos hemos escrito alguna vez y nadie disfruta leyendo. Elm no lo necesita, porque un patrón admite otro patrón en el lugar donde iría una variable, y la profundidad es libre. Conviene entender por qué esto funciona sin sintaxis adicional, y la razón es elegante: un patrón se escribe exactamente igual que se escribiría el valor que pretende casar. No es un lenguaje de consultas ni una condición disfrazada; es la misma notación de siempre, leída en la dirección contraria. Construir y deconstruir son en Elm la ida y la vuelta del mismo camino, y esta última lección del nivel trata de aprender a recorrerlo hacia atrás con soltura.

🎯 Al terminar esta lección sabrás
  • Escribir patrones anidados que casen constructores dentro de constructores en un solo nivel de case.
  • Descomponer listas con el patrón de cons y distinguir sus casos sin recurrir a funciones de acceso parciales.
  • Usar el case sobre tuplas para decidir con varios valores a la vez como si fuera una tabla.
  • Destructurar fuera de case: en argumentos, en let, sobre record por nombre de campo y con el alias as.

Un patrón es una forma, no una prueba

La regla que gobierna todo lo demás cabe en una frase: allí donde un patrón admite una variable, admite también otro patrón completo. De ahí sale el anidamiento sin necesidad de inventar nada.

type Respuesta
    = Pendiente
    | Terminada (Result String (List Usuario))


resumen : Respuesta -> String
resumen respuesta =
    case respuesta of
        Pendiente ->
            "Esperando"

        Terminada (Err motivo) ->
            "Fallo: " ++ motivo

        Terminada (Ok []) ->
            "Sin resultados"

        Terminada (Ok (primero :: _)) ->
            "El primero es " ++ primero.nombre

Cuatro ramas planas describen una estructura de tres niveles de profundidad. La última merece leerse despacio: Terminada (Ok (primero :: _)) casa con una respuesta terminada, que fue un éxito, cuya lista no está vacía, y liga la cabeza de esa lista al nombre primero, todo en un solo patrón. Tres preguntas y una extracción en una línea que no contiene ni un condicional.

El operador :: es el constructor de listas, y por eso funciona también como patrón: una lista es la vacía o es un elemento seguido de otra lista. Esa doble vida —constructor cuando construyes, patrón cuando desmontas— confirma la simetría de la que hablábamos, y tiene un efecto práctico notable. List.head devuelve un Maybe porque una lista cualquiera puede estar vacía; el patrón primero :: resto, en cambio, ya ha descartado ese caso al casar, de modo que dentro de la rama primero es un Usuario de verdad y no una opción que haya que abrir otra vez.

💡
La exhaustividad recorre los patrones anidados igual que los planos

El compilador no se conforma con que hayas cubierto el nivel exterior. Analiza el árbol completo de posibilidades y sabe que Terminada puede llevar Err u Ok, y que dentro de Ok una lista puede estar vacía o no. Si olvidas la rama de la lista vacía, el error dirá exactamente eso, señalando el patrón anidado que falta. La garantía de la lección anterior no se degrada al anidar: se propaga en profundidad, que es justo lo que hace seguro escribir patrones complejos en lugar de temerlos.

Decidir con varios valores a la vez

El anidamiento resuelve la profundidad; las tuplas resuelven la anchura. Cuando una decisión depende de dos o tres valores simultáneamente, el impulso habitual es encadenar condicionales cruzados, y ahí es donde nacen los olvidos, porque nada obliga a considerar todas las combinaciones. Un case sobre una tupla las obliga.

type Conexion
    = EnLinea
    | SinRed


type Sesion
    = Autenticado Usuario
    | Anonimo


accion : Conexion -> Sesion -> String
accion conexion sesion =
    case ( conexion, sesion ) of
        ( EnLinea, Autenticado usuario ) ->
            "Sincronizando los datos de " ++ usuario.nombre

        ( EnLinea, Anonimo ) ->
            "Mostrando el formulario de acceso"

        ( SinRed, Autenticado _ ) ->
            "Trabajando con la copia local"

        ( SinRed, Anonimo ) ->
            "Sin red y sin sesion: solo lectura"

Lo que antes era una cascada de condiciones anidadas se ha convertido en una tabla de decisión de cuatro filas que se lee de un vistazo, y a la que el compilador aplica su cuenta habitual: dos por dos son cuatro combinaciones, y si escribes tres, no compila. La ganancia no es de brevedad sino de completitud. Con condicionales, la combinación olvidada cae silenciosamente en el else de alguien; con tuplas, la combinación olvidada es un error de compilación con nombre y apellidos.

flowchart TD
P[Patron compuesto] --> A[Anidamiento en profundidad]
P --> B[Tupla en anchura]
A --> C[Un solo case en lugar de tres encajados]
B --> D[Tabla de decision con todas las combinaciones]
C --> E[Exhaustividad verificada en todo el arbol]
D --> E
style P fill:#f9e2af,color:#11111b
style E fill:#a6e3a1,color:#11111b
🪆

Anidar

Donde cabe una variable cabe un patrón entero. Tres niveles de estructura se desmontan en una sola línea.

🔗

Cons

primero :: resto distingue la lista vacía de la que no lo es y liga la cabeza sin devolver un Maybe.

🎛️

Tuplas

case ( a, b ) of convierte condicionales cruzados en una tabla cuyas filas el compilador cuenta.

🏷️

Alias as

Nombra el todo y las partes a la vez, para no tener que reconstruir lo que acabas de desmontar.

Destructurar fuera del case

El pattern matching no es exclusivo de case. Cualquier sitio donde Elm espera un nombre acepta en realidad un patrón, siempre que ese patrón sea irrefutable, es decir, que no pueda fallar porque el tipo solo admite una forma. Los record y las tuplas cumplen esa condición, y por eso se pueden desmontar en los argumentos de una función o en un let.

-- Destructuring en el argumento: entra un record, salen sus campos
saludo : { a | nombre : String, edad : Int } -> String
saludo { nombre, edad } =
    nombre ++ " tiene " ++ String.fromInt edad


-- Destructuring en let, con tuplas y con alias
distancia : ( Float, Float ) -> ( Float, Float ) -> Float
distancia origen destino =
    let
        ( x1, y1 ) =
            origen

        ( x2, y2 ) =
            destino
    in
    sqrt ((x2 - x1) ^ 2 + (y2 - y1) ^ 2)

El patrón de record casa por nombre de campo y no por posición, con lo que el orden en que escribas los campos da igual y solo mencionas los que vas a usar. La sintaxis { nombre, edad } liga dos variables con esos nombres, y el resto del record sigue ahí aunque no lo nombres.

Queda el alias, que resuelve una molestia frecuente. A veces necesitas las partes y también el todo, y sin él acabarías reconstruyendo con las manos el valor que acababas de desarmar. La palabra as permite tener ambos.

-- El alias da nombre al valor completo sin renunciar a sus partes
primeroYLista : List Usuario -> String
primeroYLista lista =
    case lista of
        [] ->
            "Vacia"

        (primero :: _) as completa ->
            primero.nombre
                ++ " encabeza una lista de "
                ++ String.fromInt (List.length completa)
ℹ️
Irrefutable es la palabra clave para saber dónde se puede destructurar

La regla que decide si un patrón vale fuera de un case es si puede fallar. Un record tiene una sola forma posible, luego destructurarlo nunca falla y Elm lo permite en argumentos y en let. Un tipo suma de tres constructores tiene tres formas, luego un patrón que mencione uno solo podría fallar, y Elm no lo admite fuera de un case porque no habría manera de garantizar la totalidad de la función. Esta coherencia no es capricho: es la misma exigencia de exhaustividad de la lección anterior aplicada a otros lugares del lenguaje. Donde no puede haber caso no cubierto, se destructura libremente; donde podría haberlo, se pasa por case.

Construir y deconstruir son las dos direcciones de un mismo lenguaje

La observación que cierra este nivel es que en Elm no hay dos notaciones, una para hacer datos y otra para leerlos, sino una sola leída en dos sentidos. Just (Ok (primero :: resto)) es una expresión válida que construye un valor y es, con los mismos caracteres, un patrón válido que lo desmonta. Esa simetría, que en la familia ML se conoce como la dualidad entre introducción y eliminación, tiene consecuencias que van bastante más allá de la comodidad sintáctica. La primera es que no hace falta aprender un lenguaje de acceso: quien sabe construir un dato ya sabe leerlo, porque escribir el patrón consiste en escribir el valor y sustituir por nombres las partes que interesan. Compáralo con la situación habitual en otros paradigmas, donde construir se hace con constructores y literales mientras leer se hace con getters, comprobaciones de tipo, encadenamiento opcional, índices, casts y una colección de operadores defensivos que hay que memorizar aparte y que ninguna simetría relaciona con la construcción. La segunda consecuencia es más honda y es la que de verdad importa: el acceso por patrón es total donde el acceso por getter es parcial. Preguntar por la cabeza de una lista con una función es una operación que puede fallar, y como puede fallar devuelve un Maybe o lanza una excepción, y en cualquiera de los dos casos el fallo se propaga hacia arriba y contamina todo lo que venga después. Casar con el patrón primero :: resto no puede fallar, porque la posibilidad de que la lista esté vacía se ha resuelto antes de entrar en la rama, en el mismo acto de casar; dentro de la rama la cabeza existe sin condiciones y sin envolturas. El pattern matching convierte sistemáticamente operaciones parciales en operaciones totales, y esa conversión es la razón técnica de que el código funcional bien escrito tenga tan pocas comprobaciones defensivas: no es que sus autores sean más valientes, es que el acceso a los datos ya incorpora la comprobación en su forma. Y hay un tercer efecto, que es el que cambia la manera de pensar y no solo la de teclear. El case sobre tuplas es una herramienta de razonamiento antes que de sintaxis, porque obliga a enumerar el producto cartesiano de las situaciones y ese ejercicio revela con incomodidad las combinaciones que uno no había considerado. Escribe la tabla de dos estados por tres modos y el compilador te exigirá seis filas; si al llegar a la cuarta descubres que no sabes qué debería ocurrir ahí, acabas de encontrar un hueco en la especificación del producto que ninguna prueba habría señalado. Eso resume el nivel entero mejor que ninguna otra cosa: type te obligó a nombrar las alternativas, case te obligó a tratarlas todas, la disciplina de los estados imposibles te obligó a que solo existieran las verdaderas, y los patrones compuestos te obligan a mirar de frente sus combinaciones. En los cuatro pasos la herramienta hace lo mismo, que es convertir preguntas de diseño que solían quedarse sin hacer en preguntas que el compilador no te deja esquivar.

⚔️ Desmonta estructuras sin escalonar el código
  1. Escribe una función sobre Maybe (Result String Int) que cubra sus cuatro casos con patrones anidados y un solo case; luego escríbela con case encajados y compara ambas versiones.
  2. Descompón una lista con [], con un solo elemento y con primero :: resto, y explica por qué dentro de la tercera rama no necesitas abrir ningún Maybe.
  3. Borra una rama de un patrón anidado y comprueba en el mensaje del compilador que la exhaustividad llega hasta el nivel interior.
  4. Escribe una función de dos tipos suma usando case sobre la tupla, cuenta las combinaciones que exige el compilador y comprueba que coinciden con el producto de sus constructores.
  5. Convierte una función que recibe un record y accede a tres campos en otra que los destructure en el argumento; compara ambas firmas en legibilidad.
  6. Usa el alias as en un patrón de lista para tener a la vez la cabeza y la lista entera, y explica qué habrías tenido que hacer sin él.
  7. Intenta destructurar un tipo suma de tres constructores en el argumento de una función, lee el error y relaciónalo con la noción de patrón irrefutable.