El bucle: un ciclo cerrado y unidireccional
Las cuatro piezas de TEA no valen nada sueltas; su valor está en cómo se conectan en un bucle. Un Msg llega, update produce un Model nuevo, view lo redibuja, y la interacción del usuario emite otro Msg que reinicia la vuelta. Esta lección explica quién cierra ese ciclo —el runtime de Elm, la única pieza impura del sistema, que es dueño de la referencia mutable al modelo actual y del DOM real— y por qué tú solo escribes funciones puras que nunca se llaman entre sí. Muestra el diff del DOM virtual como el mecanismo que hace barato redibujar desde cero, y defiende que la unidireccionalidad convierte la pregunta por qué cambió esto de una investigación en una lectura.
Conoces ya las cuatro piezas, pero una pieza quieta no es una arquitectura. TEA cobra vida cuando las cuatro se conectan en un bucle que gira siempre en el mismo sentido: un Msg entra, update lo convierte en un Model nuevo, view dibuja ese modelo, y cuando el usuario toca la pantalla nace otro Msg que arranca la vuelta siguiente. Nunca hay flujo en reversa, nunca un atajo lateral, nunca dos piezas negociando entre sí. Y hay un protagonista que no escribiste tú y que cierra el círculo: el runtime de Elm. Entender qué hace ese runtime —y qué te prohíbe hacer a ti— es entender por qué el bucle es tan predecible que depurarlo se parece más a leer que a investigar.
- Trazar el ciclo
MsgaupdateaModelaviewy de vuelta aMsg. - Identificar el runtime de Elm como la única pieza impura, dueña del estado y del DOM.
- Entender que tus funciones puras nunca se llaman entre sí: las orquesta el runtime.
- Explicar por qué la unidireccionalidad convierte la depuración en una lectura ordenada.
La vuelta completa
El ciclo tiene cuatro tramos y un solo sentido. La view produce una descripción de la interfaz que contiene, incrustados, los mensajes que cada elemento emitirá; por ejemplo, un botón que “lleva dentro” el Msg Incrementar. Cuando el usuario pulsa, el runtime recoge ese Msg y llama a update con el Msg y el Model actual. update devuelve un Model nuevo. El runtime guarda ese modelo como el estado vigente y vuelve a llamar a view con él. La nueva Html se compara con la anterior y el DOM real se actualiza. Fin de la vuelta. Si el usuario vuelve a actuar, empieza otra idéntica.
flowchart LR V[view produce Html Msg] -->|el usuario interactua| M[Msg] M --> U[update Msg Model] U -->|Model nuevo| R[runtime guarda el estado] R -->|vuelve a llamar a view| V style R fill:#cba6f7,color:#11111b style U fill:#a6e3a1,color:#11111b style M fill:#f9e2af,color:#11111b
Nota lo que no aparece en el diagrama: no hay ninguna flecha que vaya de view a update directamente, ni de Model de vuelta a Msg. Cada tramo desemboca solo en el siguiente. La interacción del usuario no es una flecha en reversa; es el nacimiento de un Msg nuevo que reinicia el circuito por su principio.
Conviene detenerse en la asimetría que esconde ese “un solo sentido”. El Model no sabe que existe la view —es un dato inerte, incapaz de notificar a nadie—. La view no sabe que existe update —solo sabe fabricar Msg—. Y update no sabe que existe la pantalla —solo transforma un valor en otro—. Cada pieza ignora a las demás; ninguna las llama ni las observa. Lo único que conoce a las tres a la vez es el runtime, que las encadena desde fuera. Esta ignorancia mutua es la que impide los caminos sorpresa: una pieza no puede tomar un atajo hacia otra si ni siquiera sabe que la otra existe.
El runtime: el único que ensucia las manos
Aquí está la revelación que reorganiza todo. Model, update y view son puros: no mutan, no guardan estado escondido, no tocan el mundo. Entonces, ¿quién sostiene la referencia al modelo actual, quién escucha los clics, quién escribe en el DOM? El runtime de Elm, una pieza escrita en JavaScript que tú no ves ni programas, y que es la única parte impura del sistema. El runtime es dueño de una variable mutable —el modelo vigente—, del bucle de eventos y del DOM real. Tu código es un conjunto de funciones puras; el runtime es el motor que las hace girar.
Esto tiene una consecuencia que sorprende a quien llega de otros paradigmas: tus funciones nunca se llaman entre sí. Tú no invocas update desde view, ni view desde update. Las escribes, se las entregas al runtime, y es él quien decide cuándo llamar a cada una y con qué argumentos. Inviertes el control por completo.
-- Tu no escribes esto; es el esqueleto conceptual de lo que hace el runtime
-- estadoActual es la UNICA variable mutable, y vive en el runtime, no en tu codigo
procesar mensaje =
let
nuevoModelo =
update mensaje estadoActual
in
guardar nuevoModelo
pintar (view nuevoModelo)
Tú aportas las funciones puras; el runtime aporta la mutación, el tiempo y el mundo. Esa división del trabajo es precisamente lo que te permite razonar sobre tu código como sobre matemáticas: la única variable que cambia no está en tu programa.
El diff del DOM virtual hace barato redibujar
Surge una objeción natural: si view recalcula la interfaz entera en cada vuelta, ¿no es carísimo repintar todo el DOM a cada clic? La respuesta es que view no produce DOM, sino una descripción ligera de él —un DOM virtual—, y el runtime compara la descripción nueva con la anterior y aplica al DOM real solo las diferencias mínimas. Tú programas como si redibujaras todo desde cero; el runtime se encarga de que en la práctica solo cambie el nodo de texto que de verdad cambió.
Esa separación es liberadora. Como view es una función pura del modelo, nunca tienes que preguntarte “¿qué trozo de la pantalla debo actualizar tras este evento?”. Describes cómo se ve la aplicación para un Model dado y punto; el problema de la actualización mínima deja de ser tuyo y pasa a ser del algoritmo de diff.
El bucle real en Elm actual
Browser.sandbox esconde el runtime para que solo veas tus funciones puras, pero conviene mirar una vez la versión completa —Browser.element— porque ahí el ciclo se hace explícito y asoman las dos vías por las que el mundo entra en el bucle: los Cmd que update devuelve y las Sub que declaras. Le entregas cuatro valores y el runtime monta la máquina.
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
init : () -> ( Model, Cmd Msg )
init _ =
( inicial, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Time.every 1000 Tic
No escribes ni un bucle, ni un addEventListener, ni una llamada a render. Aportas el estado inicial, la transición, la vista y las suscripciones; el runtime escucha, llama a update, guarda el modelo, llama a view, aplica el diff y vuelve a esperar. Programar en TEA es rellenar los huecos de un motor que ya existe, no escribir el motor. Y como el runtime es el único con acceso al modelo y al DOM, no hay dos rutas por las que el estado pueda cambiar: hay una, y pasa por él.
Hay una propiedad silenciosa en esta forma de cerrar el bucle: entre una vuelta y la siguiente, el sistema está completamente quieto. No hay animaciones de estado a medio aplicar ni actualizaciones en vuelo dentro de tu código; hay un Model estable, y el siguiente Model estable, y un salto atómico entre ambos gobernado por el runtime. Esa quietud entre vueltas es lo que hace que fotografiar el estado en cualquier instante tenga sentido: siempre hay un valor completo y coherente que fotografiar, nunca un estado a mitad de mutar.
Llega un Msg
El runtime recoge el mensaje que emitió la vista, o que produjo un Cmd o una Sub. Es el único disparador posible de una vuelta.
update transforma
El runtime llama a update con el Msg y el modelo vigente. Obtiene un Model nuevo y, quizá, un Cmd que ejecutará después.
view redibuja
El runtime pasa el nuevo modelo a view, obtiene una Html y la compara con la anterior para tocar en el DOM solo lo mínimo.
y vuelve a esperar
El ciclo se cierra. Nada corre hasta el próximo Msg. Entre vueltas hay una foto exacta y coherente del estado.
El cambio mental más valioso que regala TEA es dejar de programar transiciones visuales —“cuando llegue el dato, oculta el spinner y muestra la lista”— y empezar a describir estados completos —“así se ve la app cuando está cargando, así cuando tiene datos, así cuando falló”—. view es una función total del Model: para cada estado posible, una imagen. El runtime se encarga de ir de una imagen a la siguiente. Quien interioriza esto deja de perseguir bugs de “la pantalla se quedó a medias”, porque ya no hay medios pasos: solo estados, cada uno con su render.
El valor último del bucle cerrado no es estético ni de rendimiento: es epistemológico, tiene que ver con lo que puedes llegar a saber sobre tu propio programa. En una arquitectura con flujo bidireccional —el viejo MVC, el data-binding en dos sentidos— la pregunta “¿por qué este valor tiene ahora este contenido?” es una investigación que puede no terminar: hay que rastrear hacia atrás cada flecha entrante, y como forman ciclos, el rastreo se muerde la cola. En TEA esa misma pregunta es una lectura de una lista ordenada. El estado solo pudo cambiar dentro de update, y update solo se ejecuta por un Msg; luego basta con leer la secuencia de mensajes para reconstruir, paso a paso y sin ambigüedad, cómo se llegó al estado presente. Se ha rebajado la depuración de un problema de búsqueda en un grafo a una consulta en un registro secuencial, y eso no es una mejora incremental, es un cambio de categoría del problema. De aquí, además, cuelga el determinismo: como update es pura y los Msg son datos, la misma secuencia de mensajes sobre el mismo modelo inicial produce siempre el mismo modelo final, propiedad que hace posible grabar una sesión, reproducirla en tu máquina y viajar en el tiempo entre estados. Y todo ello se sostiene sobre una sola decisión geométrica: que la información viaje en un único sentido y que exista un único punto —el runtime— donde el estado cambia. Redux copiaría esta geometría con su store único y su dispatch, TCA la reencarnaría con su Store y su send, MVI con su stream de intents; ninguno inventó nada nuevo, todos reconocieron que un bucle unidireccional con un solo punto de mutación es la forma mínima que hace un sistema interactivo razonable. La flexibilidad de mil caminos era libertad para el que escribe y cárcel para el que depura; el sentido único es una pequeña disciplina para el que escribe y libertad total para el que entiende.
- Dibuja el ciclo
MsgaupdateaModelaviewpara un clic concreto de una app tuya y anota qué valor exacto viaja por cada tramo. Verifica que en ningún tramo la información retrocede. - Localiza en tu mente dónde vive la única variable mutable de una app Elm. Explica por qué el hecho de que esté en el runtime y no en tu código te deja razonar sobre tus funciones como sobre ecuaciones.
- Argumenta por qué
viewpuede permitirse recalcular la interfaz entera en cada vuelta sin que sea caro. Nombra la pieza que hace barato ese “redibujar todo”. - Reescribe mentalmente una lógica de “muestra u oculta el spinner” como una
viewque es función total del estado (Cargando,Listo,Fallo). Nota cómo desaparecen los medios pasos. - Completa la frase “en TEA, para saber por qué el estado es el que es, no investigo: leo…”. Si no puedes cerrarla, aún no ves la unidireccionalidad como una lectura.