elm-css: escribir hojas de estilo con tipos y pagar por la abstracción
Si el problema del enfoque clásico es que la frontera entre el programa y su aspecto está hecha de cadenas de texto que nadie verifica, la respuesta más directa consiste en mover esa frontera hacia dentro: dejar de escribir CSS en otro lenguaje y escribirlo en el propio, con funciones en lugar de propiedades, con valores tipados en lugar de literales y con el compilador vigilando la unión. Eso es exactamente lo que hace `elm-css`, y esta lección lo estudia por completo: cómo se representa una declaración como valor, por qué las unidades tienen tipos distintos y qué clase de errores desaparecen al tenerlos, cómo se componen conjuntos de estilos igual que se componen funciones, y de qué manera el árbol de vista debe cambiar de módulo para que todo esto funcione. Se examina después el precio, que es real y se paga en varios sitios a la vez: un tipo de nodo paralelo que contamina toda la aplicación hasta su borde, una conversión obligatoria en la raíz, una traducción mental permanente entre dos vocabularios y una divergencia con el resto del ecosistema que se nota el día en que hace falta una biblioteca ajena.
La solución obvia a un problema de costura es eliminar la costura, y en un lenguaje con un sistema de tipos tan expresivo como el de Elm la tentación es irresistible: si el CSS es un lenguaje pequeño, con una gramática regular y un conjunto acotado de propiedades y valores, nada impide modelarlo como tipos de datos y escribirlo como se escribe cualquier otra estructura. Una declaración deja de ser una línea de texto para ser un valor de un tipo concreto, una regla deja de ser un bloque delimitado por llaves para ser una lista de esos valores, y la relación entre un nodo y su aspecto deja de ser un nombre que viaja por el aire para ser una referencia directa que el compilador puede seguir. Lo notable de elm-css es que esta ambición se cumple casi por entero: el compilador acaba sabiendo de tu hoja de estilos tanto como sabe de tu lógica de dominio, con las mismas consecuencias sobre el refactor, la eliminación de código muerto y la detección de errores. Lo instructivo es descubrir dónde aparece la factura, porque no aparece donde uno esperaría, y porque su forma revela una ley general sobre lo que cuesta absorber un lenguaje ajeno dentro del propio.
- Representar declaraciones, unidades y colores como valores tipados y explicar qué errores se vuelven imposibles al hacerlo.
- Componer y reutilizar conjuntos de estilos con las mismas herramientas con que se componen funciones en el lenguaje.
- Situar el tipo de nodo paralelo que introduce la biblioteca y entender por qué obliga a convertir en el borde de la aplicación.
- Evaluar con honestidad el coste de la abstracción: ergonomía, divergencia de ecosistema y zonas del CSS que la biblioteca no cubre.
La declaración como valor y la unidad como tipo
El punto de partida de elm-css es que una declaración de estilo sea un valor de tipo Style, producido por una función cuyo nombre es el de la propiedad y cuyos argumentos son valores tipados. Escribir un relleno de dieciséis píxeles no es escribir una cadena sino aplicar la función padding a la aplicación de la función px sobre el número dieciséis. La diferencia parece cosmética hasta que se observa qué pasa con los errores. Una unidad de longitud y un número sin unidad tienen tipos distintos, de modo que la omisión clásica que deja una medida sin sufijo deja de compilar. Un color y una longitud tienen tipos distintos, de modo que no se puede poner uno donde va el otro. Y el nombre de la propiedad es un identificador del lenguaje, no un texto, de modo que una errata es un nombre no definido y el compilador la señala con la precisión con que señala cualquier otra.
import Css exposing (..)
import Html.Styled exposing (button, div, text)
import Html.Styled.Attributes exposing (css)
estiloBoton : List Style
estiloBoton =
[ padding2 (rem 0.5) (rem 1)
, borderRadius (px 4)
, border zero
, backgroundColor (hex "89b4fa")
, color (hex "11111b")
, cursor pointer
, hover [ backgroundColor (hex "b4befe") ]
]
verBoton : msg -> String -> Html.Styled.Html msg
verBoton alPulsar etiqueta =
button [ css estiloBoton ] [ text etiqueta ]
Hay un detalle del diseño de la biblioteca que conviene mirar despacio porque es donde vive casi toda su inteligencia. Muchas propiedades del CSS aceptan familias de valores solapadas pero no idénticas: auto vale para un margen pero no para un color, none vale para un borde pero no para una anchura numérica. elm-css codifica esas familias con tipos que llevan variables de registro extensible, de manera que cada función de propiedad declara exactamente qué conjunto de valores admite y el compilador rechaza las combinaciones que el estándar no contempla. El resultado es que una parte considerable de la especificación del CSS, que normalmente se aprende a base de probar y mirar el inspector, queda embebida en firmas y se comprueba antes de ejecutar nada.
La verificación de nombres es la ganancia más visible y la menos importante. Las tres que de verdad cambian el trabajo diario son otras. La primera es que renombrar un conjunto de estilos es una operación del compilador y no una búsqueda de texto, con la misma seguridad que renombrar una función. La segunda es que un estilo que ya nadie usa deja de ser una regla huérfana en un archivo que nadie se atreve a tocar y pasa a ser código no alcanzable, que el compilador elimina y las herramientas señalan. La tercera es que un estilo puede depender del modelo con toda naturalidad, porque es una expresión del lenguaje y no un texto precompilado.
Componer estilos es componer valores
Si un conjunto de estilos es una lista de valores, entonces todo lo que Elm sabe hacer con listas y funciones vale también para el aspecto, y esa es la segunda gran consecuencia del enfoque. Un estilo base se declara una vez y se concatena con las variaciones; una variación puede ser el resultado de una función que recibe parte del modelo; un conjunto de decisiones de diseño —la escala de espaciado, la paleta, los tamaños de letra— se declara como un módulo de constantes tipadas del que todo lo demás depende. Lo que en el mundo del preprocesador se resuelve con variables, mezclas y herencia de selectores, aquí no necesita ninguna construcción nueva: son funciones, listas y aplicación parcial, exactamente las mismas herramientas que ya usas para todo lo demás.
module Tema exposing (acento, espacio, superficie, texto)
import Css exposing (Color, Rem, hex, rem)
espacio : Int -> Rem
espacio n =
rem (0.25 * toFloat n)
acento : Color
acento =
hex "cba6f7"
superficie : Color
superficie =
hex "1e1e2e"
texto : Color
texto =
hex "cdd6f4"
tarjeta : Bool -> List Style
tarjeta destacada =
[ padding (Tema.espacio 4)
, backgroundColor Tema.superficie
, color Tema.texto
, borderRadius (px 8)
]
++ (if destacada then
[ borderLeft3 (px 3) solid Tema.acento ]
else
[]
)
El precio estructural aparece precisamente aquí, y no en la sintaxis. Para que un nodo pueda llevar estilos como valores hace falta que el nodo sea de un tipo que sepa transportarlos, y ese tipo no es el Html msg del ecosistema sino el Html msg del módulo Html.Styled, que es otro tipo distinto con el mismo nombre corto. La consecuencia se propaga hacia arriba sin excepción: toda función de vista que participe en un árbol con estilos debe devolver el tipo paralelo, todos los atributos deben venir del módulo paralelo, y en la raíz del programa hace falta una conversión explícita que traduzca el árbol al que el runtime espera. No es una molestia grave, pero es una decisión que se toma una vez y afecta a cada archivo de vista del proyecto.
import Html.Styled exposing (toUnstyled)
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view >> toUnstyled
}
Errores antes de ejecutar
Unidades, colores y nombres de propiedad son tipos. Las combinaciones que el estándar no admite no llegan a compilar.
Refactor y poda
Renombrar es seguro y lo no usado se elimina. La hoja de estilos deja de crecer monótonamente por miedo a borrar.
Doble tipo de nodo
El árbol de vista entero cambia de módulo y hay que convertir en la raíz. La decisión contagia a todos los archivos.
Divergencia de ecosistema
Las bibliotecas de terceros devuelven el nodo estándar. Integrarlas exige puentes en cada punto de contacto.
Dónde se paga y qué queda fuera
La factura tiene varias líneas y conviene leerlas todas antes de firmar. La primera es de ergonomía: el vocabulario ya no es el del estándar sino su traducción a nombres válidos de Elm, con las abreviaturas que impone la ausencia de sobrecarga, de modo que una propiedad de cuatro valores tiene una función distinta de la de dos y hay que recordar cuál es cuál. Todo el conocimiento acumulado sobre CSS sigue sirviendo, pero pasa por una capa de traducción constante, y buscar en la documentación de la web deja de dar respuestas copiables. La segunda es de integración: cualquier biblioteca de la comunidad que produzca vistas devolverá el nodo estándar, así que habrá que envolverla, convertirla o renunciar a ella. La tercera es de peso y de generación: los estilos se emiten en ejecución, con las implicaciones que eso tiene para el renderizado en servidor y para el tamaño del artefacto.
Queda además una zona del CSS que la biblioteca cubre con más esfuerzo que el resto y que conviene conocer antes de comprometerse. Las consultas de medios y las pseudoclases están soportadas y se expresan como funciones que anidan listas de estilos, lo cual funciona bien. Las animaciones con fotogramas, los selectores que dependen de la relación entre hermanos y las propiedades personalizadas del navegador encajan peor, porque su naturaleza es global o relacional y el modelo de la biblioteca es local al nodo. No son imposibles, pero se resuelven con construcciones menos elegantes que el resto, y merece la pena comprobar de antemano si tu diseño depende mucho de ellas.
flowchart TD A[Valores de tipo Style] --> B[Lista compuesta] B --> C[Atributo css en el nodo] C --> D[Arbol Html Styled] D --> E[toUnstyled en la raiz] E --> F[Arbol estandar] F --> G[Runtime de Elm] A --> H[Compilador verifica unidades y colores] style A fill:#a6e3a1,color:#11111b style E fill:#f9e2af,color:#11111b style H fill:#89b4fa,color:#11111b
Hay una lección aquí que trasciende por completo al estilo y que reaparece cada vez que un ecosistema decide internalizar una tecnología vecina. El movimiento es siempre el mismo y siempre resulta atractivo: existe un lenguaje externo con el que hay que coordinarse a través de texto sin verificar, y alguien propone modelarlo como valores del lenguaje anfitrión para que el compilador pueda hablar de él. Ocurrió con las consultas a bases de datos y sus constructores tipados, ocurrió con la generación de documentos, ocurrió con la configuración de infraestructura, y ocurre aquí con el CSS. La promesa se cumple, y esa es la parte que conviene reconocer sin escepticismo: el error se detecta antes, el refactor se automatiza, el código muerto se elimina, la composición usa las herramientas que ya conoces. Pero hay una regularidad que casi nunca se enuncia y que este caso deja ver con inusual claridad: la frontera no desaparece, se desplaza. Antes estaba en el nombre de la clase, repartida en cientos de puntos pequeños y todos verificables por inspección visual; ahora está concentrada en un punto único, la conversión de la raíz, y en una segunda frontera menos visible que es la que separa a tu proyecto del ecosistema que no adoptó la misma decisión. Esa segunda frontera es la que suele decidir el destino de estas bibliotecas, porque su coste no se paga el primer día sino el día en que necesitas un componente que alguien más escribió, y para entonces la elección ya está tomada en cada archivo. La conclusión práctica no es que internalizar sea malo, sino que la pregunta correcta al evaluar una abstracción de este tipo nunca es cuánto mejora lo que toca, sino cuántos puntos de contacto tiene con lo que no toca, y quién los mantendrá.
- Toma una hoja de estilos real de tamaño medio y traduce a
elm-cssla parte correspondiente a un componente; anota cuántas veces tuviste que consultar la documentación por diferencias de vocabulario. - Introduce deliberadamente una medida sin unidad y un color donde va una longitud; compara los mensajes con el silencio que producirían en CSS.
- Extrae la paleta y la escala de espaciado a un módulo de tema y comprueba qué ocurre al cambiar un valor central.
- Escribe una variación de estilo que dependa de un campo del modelo y razona por qué esto es incómodo en el enfoque clásico.
- Convierte el árbol completo de una pantalla al tipo paralelo y cuenta cuántos archivos tuviste que tocar por el cambio de módulo.
- Integra una biblioteca de terceros que devuelva el nodo estándar y describe con precisión el puente que hizo falta.
- Enuncia el criterio por el cual recomendarías o desaconsejarías esta biblioteca a un equipo, en función del tamaño del proyecto y de su dependencia del ecosistema.