type alias: nombrar un record y recibir un constructor de regalo
Un type alias no crea un tipo nuevo: crea un nombre para uno que ya existe, y esa distinción, que parece menor, gobierna todo su comportamiento. Esta lección desarrolla qué es exactamente un alias en Elm, por qué es intercambiable con el tipo al que apunta y por qué eso lo hace inútil para prohibir confusiones entre valores del mismo tipo estructural. Se explica el caso central del alias sobre un record, la propiedad más sorprendente del mecanismo —que al declarar un alias de record obtienes gratis una función constructora del mismo nombre, con los campos como parámetros en el orden en que fueron declarados— y sus consecuencias prácticas al decodificar JSON o al construir valores en cadena. Se cubren la actualización inmutable de records, los records extensibles como contrato parcial que exige unos campos y no dice nada de los demás, la imposibilidad de la recursividad en un alias, y el criterio para decidir cuándo un alias basta y cuándo hace falta un custom type de verdad.
La palabra alias hay que tomarla en serio, porque dice literalmente lo que la construcción hace. Un type alias no fabrica un tipo nuevo: bautiza uno que ya existía. Cuando escribes que Usuario es un record con nombre y edad, no has creado una entidad distinta de ese record, has creado una abreviatura para escribirlo. En todas partes donde el compilador vea Usuario sustituirá mentalmente el record completo, y en todas partes donde vea el record completo aceptará que lo llames Usuario. Son la misma cosa con dos nombres. De esa identidad se derivan sus dos caras: la buena, que el alias es puramente ergonómico y nunca introduce fricción ni conversiones; y la incómoda, que no puede impedir que confundas dos valores que se parecen. En medio queda la propiedad más útil y menos anunciada del mecanismo: dar nombre a un record te regala, sin pedirlo, una función que construye ese record.
- Entender que un
type aliasnombra un tipo existente y por eso es intercambiable con él en cualquier posición. - Usar el alias de un record como función constructora y saber en qué orden espera los campos.
- Actualizar records de forma inmutable y leer la sintaxis de actualización sin confundirla con una mutación.
- Distinguir cuándo un alias es suficiente y cuándo el problema exige un custom type con identidad propia.
Un alias nombra, no crea
Empecemos por la mecánica, que es simple, y por su consecuencia, que no lo es tanto. Un alias puede apuntar a cualquier tipo: uno primitivo, una tupla, una función, una lista o un record. Y en todos los casos lo que obtienes es un sinónimo perfecto, no un envoltorio.
-- Un alias puede nombrar cualquier tipo existente
type alias Metros = Float
type alias Punto = ( Float, Float )
type alias Validador = String -> Bool
type alias Usuario =
{ nombre : String
, edad : Int
, activo : Bool
}
Que Metros y Float sean intercambiables significa que puedes pasar un Float donde se pide un Metros y al revés, sin conversión y sin aviso. El alias documenta la intención, pero no la impone. Y como Segundos también sería un Float, nada impide sumar metros con segundos: para el compilador ambos son literalmente el mismo tipo. Este es el límite exacto de la construcción, y conocerlo evita el desengaño de creer que se ha modelado algo cuando solo se ha renombrado.
Si tienes type alias IdUsuario = String y type alias IdPedido = String, el compilador aceptará sin rechistar que pases uno donde se espera el otro, porque ambos son String y punto. Para que esa confusión sea imposible hace falta un custom type, que sí crea un tipo nuevo: type IdUsuario = IdUsuario String. Ese envoltorio de una sola variante obliga a construir y a desempaquetar explícitamente, y a cambio hace que mezclar identificadores no compile. La regla práctica es sencilla: usa alias para abreviar formas, usa custom types para crear distinciones.
El alias de un record también es una función
Aquí está la propiedad que sorprende a casi todo el mundo y que resulta ser la más usada en la práctica. Al declarar un type alias de un record, Elm define además una función con ese mismo nombre, cuyos parámetros son los campos en el orden exacto en que los escribiste y cuyo retorno es el record completo. Un solo nombre, dos significados según el contexto: en posición de tipo es el record, en posición de valor es su constructor.
-- El alias Usuario define ademas esta funcion, sin escribirla tu
-- Usuario : String -> Int -> Bool -> Usuario
ada : Usuario
ada =
Usuario "Ada" 36 True
-- Equivale exactamente a construirlo campo a campo
adaLiteral : Usuario
adaLiteral =
{ nombre = "Ada", edad = 36, activo = True }
Como ese constructor es una función corriente, hereda todo lo que sabemos de las funciones: se puede aplicar parcialmente, se puede pasar como argumento y se puede usar de eslabón en cualquier tubería. Ahí está el motivo de que los decodificadores de JSON en Elm se escriban como se escriben, encadenando campos sobre el nombre del alias: cada campo decodificado aplica un argumento más al constructor hasta completarlo.
flowchart LR A[type alias Usuario] --> T[Tipo: el record] A --> F[Valor: funcion constructora] F -->|aplicar nombre| P1[Falta edad y activo] P1 -->|aplicar edad| P2[Falta activo] P2 -->|aplicar activo| U[Usuario completo] style F fill:#cba6f7,color:#11111b style U fill:#a6e3a1,color:#11111b
El caso canónico donde esa aplicación progresiva se vuelve indispensable es la decodificación de JSON. Un decodificador no tiene acceso a los campos todos a la vez: los va extrayendo uno a uno, y cada extracción puede fallar. La forma de ensamblarlos consiste en partir del constructor del alias e ir alimentándolo con un campo en cada paso, exactamente como muestra el diagrama anterior.
-- El constructor del alias es el esqueleto del decodificador
decUsuario : Decoder Usuario
decUsuario =
Decode.map3 Usuario
(Decode.field "nombre" Decode.string)
(Decode.field "edad" Decode.int)
(Decode.field "activo" Decode.bool)
Si mañana añades un campo al alias, ese código deja de compilar en el acto: el constructor pasa a esperar un argumento más y map3 se queda corto. Es un ejemplo perfecto de garantía estructural obtenida sin escribir ninguna comprobación, y solo es posible porque el nombre del alias es a la vez el tipo y la función que lo construye.
El constructor generado espera los campos en el orden de la declaración, no en orden alfabético ni en el que a ti te parezca. Reordenar los campos de un alias es, por tanto, un cambio que puede romper llamadas existentes sin que el compilador siempre lo detecte, si los tipos coinciden. Cuando varios campos comparten tipo, la construcción literal con nombres es más segura que la posicional, aunque sea más larga.
Actualizar sin mutar
Un record en Elm es inmutable, de modo que no existe la operación de cambiar un campo. Lo que existe es una sintaxis que produce un record nuevo, idéntico al anterior salvo en los campos que enumeras. Se lee como una modificación y no lo es en absoluto: el valor original sigue intacto y disponible, y esa es la razón de que la historia de estados de una aplicación Elm pueda guardarse entera sin miedo.
-- Produce un record NUEVO; el original no cambia
cumplirAnios : Usuario -> Usuario
cumplirAnios u =
{ u | edad = u.edad + 1 }
-- Varios campos a la vez, separados por comas
desactivarYRenombrar : String -> Usuario -> Usuario
desactivarYRenombrar nuevo u =
{ u | nombre = nuevo, activo = False }
La sintaxis exige que el valor a la izquierda de la barra sea un nombre, no una expresión cualquiera, y no permite añadir campos que el record no tuviera ni quitarlos: solo actualiza los existentes. Esa restricción es deliberada y garantiza que el tipo del resultado sea idéntico al del original, lo que a su vez permite que update en la Arquitectura Elm devuelva siempre un Model bien formado por construcción.
Sinónimo perfecto
Un alias y su tipo son intercambiables en cualquier posición. No hay conversión, no hay coste y tampoco hay protección frente a confusiones.
Constructor gratis
Todo alias de record define una función homónima con los campos como parámetros. Es la pieza sobre la que se apoyan los decodificadores de JSON.
Actualización inmutable
La sintaxis de barra vertical crea un record nuevo con los campos indicados cambiados. El original permanece, intacto y utilizable.
Record extensible
Un alias con una variable de fila describe cualquier record que tenga al menos esos campos, y deja libres los demás.
Records extensibles y los límites del alias
Un alias puede llevar parámetros de tipo, y el caso más interesante es el de la variable de fila. Escribir un alias con una minúscula antes de la barra significa cualquier record que tenga al menos estos campos. Con eso se escriben funciones que exigen justo lo que necesitan y nada más, sin acoplarse a un record concreto.
-- Alias parametrizado por el resto del record
type alias ConNombre a =
{ a | nombre : String }
-- Sirve para Usuario, para Producto y para cualquiera con nombre
etiquetar : ConNombre a -> String
etiquetar x =
"Nombre: " ++ x.nombre
-- Un alias NO puede referirse a si mismo: esto no compila
-- type alias Nodo = { valor : Int, hijos : List Nodo }
El último comentario señala la limitación estructural más importante. Como el alias se expande textualmente, una definición recursiva provocaría una expansión infinita y el compilador la rechaza. Los árboles, las listas propias y cualquier estructura autorreferencial exigen un custom type, porque este sí introduce un nombre genuinamente nuevo que puede aparecer dentro de su propia definición. Esa frontera es también el mejor criterio para elegir entre uno y otro.
La lección conceptual del type alias es una que trasciende Elm y que se ignora con enorme frecuencia en todos los lenguajes: hay dos operaciones de tipado que la sintaxis suele confundir y que son ontológicamente distintas. Una es la abreviatura, que toma una estructura existente y le da un nombre corto para poder hablar de ella; la otra es la creación, que introduce en el universo del programa una entidad que antes no existía y que, por definición, no se confunde con ninguna otra. El type alias hace lo primero y solo lo primero. Su semántica es la de una macro de expansión textual sobre el vocabulario de tipos: el compilador la deshace antes de razonar, y por eso IdUsuario e IdPedido no son dos cosas, son la misma cosa dicha de dos maneras. La confusión entre ambas operaciones es la fuente de una clase entera de errores de diseño, en la que un equipo cree haber modelado un dominio porque ha inventado nombres, cuando en realidad no ha creado ninguna distinción que el compilador pueda defender. La prueba para saber en cuál de las dos estás es única y muy simple: pregúntate si el compilador impediría intercambiar dos valores de esos tipos supuestamente distintos. Si la respuesta es no, tienes un vocabulario más rico, no un modelo más fuerte, y toda la seguridad reside en la disciplina humana. Elm es honesto en este punto porque separa ambas construcciones con dos palabras clave distintas y no permite fingir. Cuando eliges type alias estás diciendo con precisión que esto es lo mismo escrito más corto; cuando eliges type estás diciendo que esto es una cosa nueva en el mundo. La madurez con los tipos empieza el día en que esa elección se hace consciente y deliberada, y no por costumbre.
- Declara un alias de record con cuatro campos y construye un valor de las dos maneras: con el constructor posicional y con la sintaxis literal.
- Anota a mano la firma exacta de la función constructora que el alias generó, y comprueba el orden de los parámetros.
- Aplica parcialmente ese constructor dejando el último campo sin dar y explica qué tipo tiene el valor resultante.
- Escribe una función de actualización inmutable de un campo y demuestra que el record original permanece intacto tras usarla.
- Crea
IdUsuarioeIdPedidocomo alias deString, comprueba que se pueden intercambiar, y arréglalo con custom types de una sola variante. - Intenta declarar un alias recursivo para un árbol, lee el error del compilador y explica por qué la expansión textual lo hace imposible.