wandres.dev
HTML COMO FUNCIÓN · la vista pura

elm/html: el elemento como función ordinaria

Casi todas las bibliotecas de interfaz de la última década resuelven el mismo problema de la misma manera: inventar un idioma nuevo para describir la vista, sea una plantilla con directivas, sea una extensión sintáctica como JSX. Elm toma la decisión contraria y no añade nada. La biblioteca elm/html expone cada elemento del documento como una función corriente con una firma uniforme que recibe una lista de atributos y una lista de hijos y devuelve un árbol tipado. Esta lección desarrolla esa firma hasta sus consecuencias: por qué la uniformidad convierte el anidamiento en aplicación de funciones, por qué no hace falta ninguna directiva de repetición o de condición cuando el lenguaje anfitrión ya tiene funciones y expresiones, cómo se factoriza una vista extrayendo funciones auxiliares sin ningún mecanismo de componentes, y qué significa el parámetro de tipo en Html msg, que declara qué vocabulario de mensajes puede emitir un fragmento de interfaz y permite escribir árboles inertes con Html Never.

⏱ 16 min

La historia de las interfaces web es en buena medida la historia de los idiomas que hemos inventado para describirlas. Las plantillas de Angular y de Vue añaden un dialecto con directivas propias para repetir, condicionar y enlazar. JSX añade una extensión sintáctica al propio JavaScript que un compilador traduce después a llamadas de función. Los sistemas de plantillas de Handlebars o Jinja añaden un lenguaje reducido que se interpreta sobre cadenas de texto. Todos parten del supuesto de que describir una interfaz requiere una notación especial, distinta de la que usas para el resto del programa. Elm no añade nada. La biblioteca elm/html no aporta sintaxis, ni un preprocesador, ni una fase de compilación adicional: aporta un puñado de funciones ordinarias, escritas en Elm, invocadas como cualquier otra función, con la misma firma unas y otras. El árbol del documento no es una plantilla que se procesa, es el valor que devuelve una expresión. Esa decisión, aparentemente modesta, es la que hace que toda la potencia del lenguaje esté disponible dentro de la vista sin que nadie tenga que reimplementarla en miniatura.

🎯 Al terminar esta lección sabrás
  • Leer la firma uniforme de los elementos de elm/html y entender por qué recibe dos listas y no un mapa de propiedades.
  • Explicar por qué el anidamiento del documento coincide exactamente con el anidamiento de las llamadas a función.
  • Factorizar una vista extrayendo funciones auxiliares, sin ningún mecanismo de componentes ni registro de etiquetas.
  • Interpretar el parámetro de tipo de Html msg y el uso de Html Never para fragmentos que no emiten nada.

Una sola firma para todo el documento

La biblioteca declara una función por cada elemento del lenguaje de marcado, y todas comparten la misma forma: reciben una lista de atributos, reciben una lista de hijos y devuelven un nodo. No hay excepciones caprichosas ni firmas especiales para elementos especiales. Un div, un button, un section y un li se construyen exactamente igual, de modo que aprender uno es aprender todos. La única función con forma distinta es text, que convierte una cadena en un nodo de texto y por tanto no admite ni atributos ni hijos, porque un nodo de texto no puede tenerlos.

-- Toda la biblioteca cabe en una firma repetida
div    : List (Attribute msg) -> List (Html msg) -> Html msg
button : List (Attribute msg) -> List (Html msg) -> Html msg
ul     : List (Attribute msg) -> List (Html msg) -> Html msg
text   : String -> Html msg


-- Anidar el documento es anidar llamadas
tarjeta : String -> String -> Html msg
tarjeta titulo cuerpo =
    div [ class "tarjeta", id "principal" ]
        [ h2 [ class "titulo" ] [ text titulo ]
        , p [] [ text cuerpo ]
        ]

Observa que la sangría del código y la jerarquía del documento coinciden sin que nadie lo haya impuesto: la lista de hijos de un nodo contiene las llamadas que construyen esos hijos, así que la estructura visual del árbol emerge de la estructura de la expresión. Cuando un elemento no lleva atributos se escribe una lista vacía, y cuando no lleva hijos también; la uniformidad se paga con esas dos listas vacías ocasionales y se cobra en que nunca hay que recordar un caso particular.

🌳

Dos listas siempre

Atributos primero, hijos después. La misma firma para cada etiqueta convierte la biblioteca entera en un solo patrón que se aprende una vez.

🔤

text es la hoja

La única función distinta: convierte una cadena en nodo de texto. No admite atributos ni hijos porque un nodo de texto no puede tenerlos.

🏷️

Atributos tipados

class, id, href o value son funciones que devuelven un Attribute msg. Un atributo mal escrito es un error de compilación, no un fallo silencioso.

🧩

node para lo demás

Si una etiqueta no está en la biblioteca, Html.node la construye a partir de su nombre. No hay etiquetas privilegiadas ni registro global.

No hay plantilla porque el lenguaje ya basta

Un sistema de plantillas existe para dar, dentro de la vista, capacidades que el lenguaje anfitrión ya tiene fuera de ella: repetir, condicionar, nombrar un fragmento, pasarle parámetros. Ese es el motivo por el que cada uno de esos sistemas termina reinventando un pequeño lenguaje de programación con su propia sintaxis de bucles, su propio ámbito de variables y sus propias reglas de evaluación, casi siempre menos expresivas y peor tipadas que las del lenguaje al que acompañan. Si la vista es una expresión ordinaria, ese lenguaje secundario deja de tener razón de ser: repetir es aplicar List.map, condicionar es un if o un case, nombrar un fragmento es definir una función y pasarle parámetros es aplicarla.

💡
La prueba del algo indefinido

Hay una manera rápida de comprobar que un sistema de plantillas es un lenguaje aparte: pregúntate qué ocurre cuando escribes mal el nombre de una variable dentro de la plantilla. En casi todos los casos la respuesta es que no ocurre nada visible y el hueco se renderiza vacío o con un texto indefinido, porque la plantilla se resuelve en tiempo de ejecución contra un contexto sin tipos. En Elm la respuesta es que el programa no compila, porque dentro de la vista no hay ningún contexto mágico: hay parámetros de una función, sujetos exactamente a las mismas reglas de alcance y de tipado que en cualquier otro punto del programa.

La consecuencia práctica es que el mecanismo de reutilización de vistas no existe como mecanismo. Cuando un fragmento se repite, extraes una función; cuando esa función necesita variar, le añades un parámetro; cuando quieres que reciba contenido arbitrario, le pasas una lista de Html msg. No hay que declarar un componente, registrarlo, decidir si es de presentación o de contenedor, ni preguntarse cómo se comunica con su padre. Refactorizar la vista es exactamente el mismo acto que refactorizar cualquier otro código: extraer, renombrar, parametrizar.

-- Un fragmento reutilizable es solo una funcion
campo : String -> List (Html msg) -> Html msg
campo etiqueta contenido =
    div [ class "campo" ]
        (label [ class "etiqueta" ] [ text etiqueta ] :: contenido)


-- Y se usa como cualquier otra funcion
formulario : Html msg
formulario =
    div [ class "formulario" ]
        [ campo "Nombre" [ input [ type_ "text" ] [] ]
        , campo "Correo" [ input [ type_ "email" ] [] ]
        ]

El tipo declara qué mensajes puede emitir

El parámetro que acompaña a Html no es decorativo. Un valor de tipo Html msg es un árbol de interfaz que, cuando el usuario interactúe con él, podrá producir valores de tipo msg. Ese parámetro viaja por toda la biblioteca: los atributos son Attribute msg porque entre ellos están los manejadores de eventos, y las listas de hijos son List (Html msg) porque los hijos emiten al mismo vocabulario que el padre. En una aplicación concreta, msg se instancia con tu tipo de mensajes y la vista tiene el tipo Html Msg, lo que significa que el compilador comprueba que ningún fragmento de la interfaz pueda producir un mensaje que update no sepa tratar.

-- Generico: sirve para cualquier vocabulario de mensajes
separador : Html msg
separador =
    hr [ class "separador" ] []


-- Inerte por tipo: nunca podra emitir nada
aviso : String -> Html Never
aviso texto =
    p [ class "aviso" ] [ text texto ]


-- Concreto: emite el vocabulario de esta aplicacion
vista : Model -> Html Msg
vista model =
    div [] [ separador, Html.map never (aviso "solo lectura") ]

El caso de Html Never merece atención porque enseña a leer el sistema de tipos como documentación ejecutable. El tipo Never no tiene ningún valor posible, así que un árbol de tipo Html Never es un árbol del que se demuestra, por construcción y sin leer una línea de su implementación, que jamás emitirá un mensaje. Es la firma natural de un texto de ayuda, de un icono decorativo o de una tabla de solo lectura, y garantiza a quien lo consume que insertarlo no puede introducir interacción inesperada. Un fragmento genérico con msg sin instanciar dice algo parecido pero más débil: no emite mensajes propios, así que encaja en cualquier vocabulario.

flowchart TD
F[Funciones de elm html] --> A[Lista de atributos]
F --> H[Lista de hijos]
A --> N[Nodo Html msg]
H --> N
N --> V[Vista completa Html Msg]
L[Lenguaje anfitrion] --> F
style F fill:#89b4fa,color:#11111b
style N fill:#a6e3a1,color:#11111b
style V fill:#cba6f7,color:#11111b
La vista no es un lenguaje aparte: es la misma expresión que el resto del programa

Detrás de la modestia de esta decisión hay una tesis fuerte sobre el diseño de lenguajes que conviene enunciar sin rodeos: cada vez que una tecnología inventa una notación especial para un dominio, está apostando a que ese dominio necesita menos expresividad que el lenguaje general, y esa apuesta casi siempre se pierde. La historia de las plantillas es la crónica de esa derrota repetida. Empiezan siendo interpolación de variables, admiten después una condición porque hacía falta, luego un bucle, luego filtros para transformar valores, luego parciales para reutilizar, luego un ámbito con reglas propias, y al final tienes un lenguaje de programación completo, mal tipado, sin depurador decente, sin las herramientas de refactorización del principal y con una frontera de traducción por la que se pierde información en cada cruce. La alternativa de Elm no es ser más elegante, es negarse a cruzar esa frontera: si la vista se escribe con funciones del lenguaje, todo lo que el lenguaje sabe hacer se aplica a la vista sin adaptador. La inferencia de tipos alcanza dentro del árbol; el compilador señala el nodo exacto donde el tipo no cuadra; extraer una función auxiliar dentro de la vista se hace con el mismo gesto y las mismas garantías que extraerla en la capa de datos; un fragmento se pasa como argumento, se guarda en una lista, se compone con List.map o se devuelve desde un case, porque no es una construcción sintáctica sino un valor de primera clase. Y hay una consecuencia epistemológica que suele pasar inadvertida: cuando la vista es un valor, deja de haber dos categorías mentales, código y marcado, con dos conjuntos de intuiciones distintas. Solo hay funciones que devuelven datos. La pregunta cómo hago esto en la plantilla se disuelve, porque la respuesta es siempre la misma que fuera de ella.

⚔️ Construye el árbol sin salir del lenguaje
  1. Escribe la firma de memoria de tres elementos distintos y comprueba que coinciden salvo por el nombre; explica por qué text es la excepción.
  2. Construye una tarjeta con título, cuerpo y pie usando solo funciones, y observa cómo la sangría del código reproduce la jerarquía del documento.
  3. Extrae una función campo que reciba una etiqueta y una lista de hijos, y úsala dos veces en un formulario sin declarar ningún componente.
  4. Da a un fragmento decorativo el tipo Html Never e intenta añadirle un manejador de evento; lee el error del compilador y explica qué te está demostrando.
  5. Emplea Html.node para construir una etiqueta que no exista en la biblioteca y razona por qué eso no requiere ningún registro global.
  6. Enumera, para un sistema de plantillas que conozcas, qué capacidades del lenguaje anfitrión tuvo que reimplementar y qué se pierde en cada traducción.