wandres.dev
CUSTOM TYPES · uniones y pattern matching

type: la unión etiquetada como herramienta de modelado

Todos los tipos vistos hasta aquí responden a la conjunción: un record dice hay un número y un texto y un booleano, y por eso se llaman tipos producto. Pero la otra mitad del modelado del mundo es disyuntiva —un pago es con tarjeta o por transferencia o en efectivo— y esa alternativa cerrada no se puede expresar con un record sin dejar la puerta abierta a combinaciones absurdas. Esta lección introduce la palabra clave type de Elm, que declara una unión etiquetada o tipo suma: un tipo nuevo cuyos valores son exactamente las alternativas enumeradas, cada una marcada con un constructor que la distingue. Desarrolla la sintaxis, la diferencia crucial entre type y type alias, el constructor como etiqueta discriminante y como valor de primera clase, y el criterio de modelado que sustituye banderas booleanas y cadenas de texto libres por alternativas nombradas y cerradas. Cierra con el álgebra de tipos —por qué producto y suma son literalmente multiplicación y adición de cardinalidades— y por qué la ausencia de sumas en los lenguajes mayoritarios explica media historia de los bugs de estado.

⏱ 18 min

Hasta ahora todos los tipos que has construido en Elm han respondido a la conjunción. Un record con tres campos dice: hay un número y un texto y un booleano. Una tupla dice lo mismo con menos ceremonia. Son tipos producto, y ese nombre no es una metáfora poética sino una cuenta exacta que veremos al final. Pero la mitad del modelado del mundo no es conjuntiva sino disyuntiva. Un pago es con tarjeta o por transferencia o en efectivo. Una petición está pendiente o ha terminado bien o ha fallado. Una figura es un círculo o un rectángulo. Esa segunda mitad —el o exclusivo, la alternativa cerrada— no cabe en un record, y forzarla a caber produce exactamente los diseños defectuosos que este nivel viene a desmontar. La palabra clave type es la respuesta de Elm: declara un tipo nuevo cuyos valores son exactamente las alternativas que enumeras, cada una marcada con una etiqueta que la separa sin ambigüedad de las demás. Se llama unión etiquetada, o tipo suma, y es sin exageración la herramienta de modelado más importante del lenguaje.

🎯 Al terminar esta lección sabrás
  • Distinguir un tipo producto —record, tupla— de un tipo suma declarado con type.
  • Leer y escribir la sintaxis de type con varios constructores y entender que el conjunto es cerrado.
  • Diferenciar type, que crea un tipo nuevo, de type alias, que solo pone un nombre a uno existente.
  • Sustituir banderas booleanas y cadenas de texto libres por alternativas nombradas al modelar un dominio.

El eje que faltaba: de la conjunción a la disyunción

Un record combina información: para tener un valor necesitas todos sus campos a la vez. Un tipo suma hace lo contrario: para tener un valor basta con uno de sus casos, y solo uno. Son los dos ejes ortogonales del modelado de datos, y cualquier estructura que sepas describir se compone de ellos en alguna proporción. Elm te dio el primero desde el principio; el segundo llega con type.

-- Producto: hay un nombre Y una edad Y un correo, todos a la vez
type alias Usuario =
    { nombre : String
    , edad : Int
    , correo : String
    }


-- Suma: es exactamente uno de estos tres casos, nunca dos, nunca ninguno
type MetodoPago
    = Tarjeta
    | Transferencia
    | Efectivo

Cada nombre a la derecha del = y de las barras verticales es un constructor. Un constructor cumple dos funciones simultáneas que conviene no confundir: es la etiqueta que discrimina el caso y es, él mismo, un valor del tipo. Escribir Tarjeta no llama a nada ni ejecuta nada; produce un valor de tipo MetodoPago cuya única propiedad interesante es ser distinguible de Transferencia y de Efectivo. No hay un número oculto detrás, ni una cadena de texto, ni un índice: la identidad del caso es la etiqueta.

Y el conjunto es cerrado. Fuera de esas tres alternativas no existe ni puede existir un cuarto valor de tipo MetodoPago; no hay forma de fabricar uno desde otro módulo, ni de convertir un String en uno por descuido, ni de que llegue uno inesperado desde el exterior. Esa clausura es lo que permitirá, en la lección tercera, que el compilador verifique que has tratado todos los casos: solo se puede exigir exhaustividad sobre un conjunto que se sabe completo.

💡
type crea un tipo nuevo; type alias solo bautiza uno que ya existe

La distinción se malinterpreta a menudo y tiene consecuencias reales. type alias Edad = Int no crea nada: Edad e Int son la misma cosa con dos nombres, se pueden intercambiar libremente y sumar una Edad a un código postal compila sin queja. type Edad = Edad Int, en cambio, crea un tipo genuinamente nuevo que el compilador se niega a confundir con Int aunque por dentro lleve uno. El primero es azúcar de legibilidad; el segundo es una barrera de tipos. Cuando quieras que el compilador impida mezclar dos cosas que casualmente se representan igual, type alias no te servirá para nada y type lo hará gratis.

Modelar con alternativas en lugar de con banderas

El valor práctico de los tipos suma se ve mejor por contraste, observando qué ocurre cuando no se tienen. Sin ellos, el modelador recurre a dos sustitutos pobres: banderas booleanas y cadenas de texto. Ambos comparten el mismo defecto estructural, que es admitir muchos más valores de los que el dominio tiene sentidos.

-- Antipatron: banderas independientes para estados que se excluyen
type alias Pedido =
    { enviado : Bool
    , entregado : Bool
    , cancelado : Bool
    }


-- Nada impide construir esto, y no significa absolutamente nada
absurdo : Pedido
absurdo =
    { enviado = False, entregado = True, cancelado = True }

Tres booleanos admiten ocho combinaciones. El dominio real tiene cuatro estados, y los cuatro se excluyen mutuamente. Las otras cuatro combinaciones son basura sintácticamente válida que el compilador acepta sin pestañear y que tú tendrás que defender a mano, con comprobaciones dispersas por toda la aplicación y comentarios que ruegan no poner los dos a True. La variante con String es peor todavía, porque el número de valores posibles pasa de ocho a infinito y una errata como "entregdo" es un valor perfectamente legal que ninguna herramienta detecta.

-- Modelado con alternativas: existen los cuatro estados reales y ni uno mas
type EstadoPedido
    = Recibido
    | Enviado
    | Entregado
    | Cancelado

El cambio no es cosmético. Antes tenías un tipo con ocho habitantes de los que cuatro eran mentira; ahora tienes un tipo con cuatro habitantes, todos verdaderos. La categoría entera de errores que consistía en dos banderas contradictorias ha dejado de ser un bug para pasar a ser algo que no se puede ni escribir. Y como el conjunto es cerrado y nombrado, la definición se lee como documentación que el compilador mantiene honesta: quien abra el archivo dentro de un año sabrá en cuatro líneas qué le puede pasar a un pedido.

flowchart TD
T[type EstadoPedido] --> A[Recibido]
T --> B[Enviado]
T --> C[Entregado]
T --> D[Cancelado]
A --> U[Un valor es exactamente uno de los cuatro]
B --> U
C --> U
D --> U
style T fill:#f9e2af,color:#11111b
style U fill:#a6e3a1,color:#11111b
🌳

type

Declara un tipo nuevo enumerando sus constructores. El conjunto de valores es cerrado y el compilador lo sabe.

🏷️

Constructor

La etiqueta que discrimina el caso y, a la vez, un valor de primera clase del tipo. No es un número ni un texto disfrazado.

✖️

Producto

El record y la tupla: todos los campos a la vez. La conjunción del modelado, y la multiplicación de sus cardinalidades.

Suma

La unión etiquetada: uno de los casos, y solo uno. La disyunción del modelado, y la adición de sus cardinalidades.

Los dos ejes no compiten entre sí; se anidan. Un tipo suma puede tener variantes que lleven productos dentro, y un producto puede tener campos cuyo tipo sea una suma. El modelado real alterna ambos según lo que el dominio pida en cada nivel, y saber en cuál de los dos ejes estás en cada momento es buena parte del oficio.

-- Suma en el nivel exterior, producto dentro de una de sus variantes
type Sesion
    = Anonima
    | Iniciada Credenciales


type alias Credenciales =
    { usuario : String
    , caducidad : Int
    }

Un String de usuario y una caducidad solo tienen sentido cuando la sesión está iniciada, y por eso viven dentro de esa variante y no al lado de ella. Si estuvieran sueltos en un record junto a una bandera, el tipo permitiría una sesión anónima con credenciales, que es literalmente una contradicción. Colocar cada dato en el nivel correcto del anidamiento es la operación que la cuarta lección de este nivel convertirá en método.

Nombrar es modelar

Hay una consecuencia menos técnica y más profesional de todo esto. Un tipo suma obliga a nombrar cada alternativa, y nombrar obliga a decidir. Cuando escribes type EstadoPedido y empiezas a enumerar constructores, estás haciendo análisis de dominio con el compilador delante: cada caso que añades es una afirmación sobre el mundo que se puede discutir con quien conoce el negocio. Los booleanos permiten posponer ese trabajo indefinidamente, y esa comodidad inicial es justamente la trampa, porque la decisión no desaparece: se difiere y se dispersa por cien condicionales que nadie volverá a leer juntos.

El criterio de diseño que sale de aquí es sencillo de enunciar y sorprendentemente exigente de aplicar. Cada vez que estés a punto de introducir un Bool en un modelo, pregúntate qué dos situaciones representa y si de verdad son solo dos; y cada vez que introduzcas dos booleanos relacionados, sospecha en firme, porque casi siempre son una suma de tres o cuatro casos disfrazada. La sospecha se generaliza: un String que solo debería tomar ciertos valores, un Int que codifica un estado, un campo llamado tipo o modo son todos síntomas del mismo hueco, que es una unión etiquetada que alguien no pudo o no quiso declarar.

ℹ️
Los nombres de constructor viven en el espacio de nombres del módulo

Un detalle práctico que conviene saber pronto: en Elm los constructores no se cualifican con el nombre del tipo, sino que viven directamente en el módulo. Eso significa que dos tipos distintos del mismo módulo no pueden compartir el nombre de un constructor, y explica los prefijos que verás en código real, del estilo EstadoCargando frente a PeticionCargando. También significa que al exponer un tipo desde un módulo puedes elegir entre exponerlo con todos sus constructores o solo con su nombre; esa segunda opción, el tipo opaco, es una herramienta de encapsulación que reaparecerá en la cuarta lección de este nivel.

La suma es la mitad que falta del álgebra de los tipos, y su ausencia explica media historia de los bugs de estado

Los nombres producto y suma son literales, no analogías, y entenderlo cambia cómo se mira un modelo de datos. Cuenta cuántos valores distintos admite un tipo y llámalo su cardinalidad: Bool tiene dos, un tipo suma de cuatro constructores sin datos tiene cuatro, un record con dos campos booleanos tiene dos por dos igual a cuatro. La regla es exacta: la cardinalidad de un producto es el producto de las cardinalidades de sus componentes, y la de una suma es su adición. Por eso tres booleanos independientes son ocho valores y cuatro constructores son cuatro, y por eso la refactorización de la sección anterior no fue una cuestión de gusto sino una reducción medible de ocho a cuatro, es decir, la eliminación exacta de las cuatro combinaciones que no significaban nada. Con esa lente, modelar deja de ser una actividad estética y se convierte en aritmética aplicada: el objetivo es que la cardinalidad del tipo coincida lo más ajustadamente posible con el número de situaciones reales del dominio, porque cada valor sobrante es un estado ilegal que alguien tendrá que defender a mano en tiempo de ejecución. Y aquí está el hecho histórico que lo enmarca todo: la inmensa mayoría de los lenguajes que dominaron la industria durante décadas —C, Java, Python, JavaScript, Go— dieron a sus usuarios productos de primera clase, con struct, clases y objetos, pero no sumas. El enum de C es una suma degenerada sin datos y sin comprobación real, la unión de C es una suma sin etiqueta y por tanto sin seguridad, y la herencia orientada a objetos se usó durante veinte años como sustituto de las sumas precisamente porque no había otra cosa, con el resultado de que la lista de subclases queda abierta y ninguna comprobación de exhaustividad es posible. La consecuencia práctica es que generaciones enteras de programadores aprendieron a modelar alternativas con lo único que tenían: banderas, códigos numéricos, cadenas mágicas, campos que solo son válidos si otro campo tiene cierto valor, y el infame null como forma pobrísima de decir o no hay nada. Media historia de los bugs de estado de la industria es la historia de esa carencia. Que Elm, Haskell, OCaml, Rust, Swift y las uniones discriminadas de TypeScript hayan convergido en devolver la suma al centro del lenguaje no es una moda funcional; es la corrección de una asimetría que nunca tuvo justificación teórica, porque el mundo se describe con conjunciones y disyunciones a partes iguales y un lenguaje que solo te da la mitad te obliga a improvisar la otra con parches. Cuando dentro de dos lecciones veas al compilador negarse a compilar por un caso que olvidaste, recuerda que esa capacidad no viene del case: viene de que el tipo era una suma cerrada, y por tanto contable.

⚔️ Modela un dominio con alternativas cerradas
  1. Declara type MetodoPago con tres constructores y escribe una función que devuelva un String descriptivo para cada uno; observa que no necesitas ningún valor por defecto.
  2. Toma el record de tres banderas booleanas de la lección y enumera sus ocho combinaciones; señala cuáles son estados reales y cuáles basura, y explica quién las defiende hoy en tu código.
  3. Reescribe ese record como un type de cuatro constructores y cuenta cuántos estados ilegales han dejado de ser expresables.
  4. Escribe type alias Edad = Int y type Anios = Anios Int, e intenta sumar cada uno a un entero cualquiera; explica con precisión qué hace distinto el compilador en cada caso.
  5. Busca en un proyecto propio un campo de texto libre que solo admite unos pocos valores y conviértelo en un tipo suma; anota qué erratas se vuelven imposibles.
  6. Calcula la cardinalidad de un record con un Bool y un tipo suma de tres constructores, y luego la de un tipo suma de dos constructores donde cada uno lleva ese Bool; comprueba que la aritmética coincide con tu intuición.