wandres.dev
ELM: EFECTOS · commands y subscriptions

El runtime de Elm: el actor invisible que ejecuta los efectos

Si update solo describe efectos con Cmd y subscriptions solo declara escuchas con Sub, alguien tiene que ejecutar de verdad las peticiones, entregar sus resultados como Msg y reconciliar las suscripciones. Ese alguien es el runtime de Elm, el código que Elm genera y envuelve alrededor de tu programa. Esta lección lo hace visible: describe el bucle que mantiene el modelo, llama a tu update pura, obtiene la tupla modelo mas comando, guarda el modelo, pinta la vista mediante el DOM virtual, ejecuta el comando tocando el mundo, y vuelve a llamar a subscriptions para ajustar los oyentes; los resultados de comandos y los eventos de suscripciones reentran como nuevos Msg cerrando el bucle. Explica el patrón núcleo funcional puro con corteza imperativa que el lenguaje impone, cómo Browser.element cablea init, update, view y subscriptions, y por qué la garantía de cero excepciones en ejecución se sigue de que el runtime sea el único actor impuro del sistema.

⏱ 18 min

Hemos dicho que update describe efectos pero no los ejecuta, y que subscriptions declara escuchas pero no engancha oyentes. La pregunta que queda es inevitable: entonces, ¿quién hace el trabajo? ¿Quién abre el socket, espera la respuesta, la envuelve en un Msg y lo despacha? ¿Quién activa y cancela los oyentes reales del navegador? La respuesta es el runtime de Elm, el actor invisible del sistema: el código que el compilador genera y coloca como una corteza alrededor de tu programa. Tú escribes cuatro cosas puras —init, update, view y subscriptions— y el runtime es todo lo demás, el motor que las orquesta y que concentra, en un único lugar ajeno a ti, toda la impureza del programa. Comprenderlo es dejar de ver la Arquitectura Elm como un conjunto de reglas y verla como lo que es: un contrato entre tu núcleo puro y un intérprete impuro que ejecuta por ti lo que tú solo te atreviste a describir.

🎯 Al terminar esta lección sabrás
  • Describir el bucle del runtime: mantener el modelo, llamar a update, pintar view, ejecutar Cmd y reconciliar Sub.
  • Entender el patrón de núcleo funcional puro rodeado de una corteza imperativa, impuesto por el lenguaje.
  • Leer cómo Browser.element cablea init, update, view y subscriptions en un programa ejecutable.
  • Explicar por qué la garantía de cero excepciones en ejecución se sigue de que el runtime sea el único actor impuro.

El bucle que nunca escribes

El runtime es una máquina de eventos que gira sin descanso y cuyo estado es tu modelo. Su ciclo puede narrarse paso a paso. Guarda en su interior el modelo actual. Cuando ocurre un evento —al arrancar, un Msg que produjo el resultado de un comando, un Msg que emitió una suscripción, o un Msg disparado por la vista— el runtime llama a tu update con ese mensaje y el modelo que custodia. Recibe de vuelta la tupla ( nuevoModelo, cmd ). Entonces hace cuatro cosas que tú nunca escribes: sustituye el modelo por el nuevo, llama a view nuevoModelo y aplica al DOM real el diff con el DOM virtual anterior, ejecuta el cmd realizando la entrada y salida de verdad, y llama a subscriptions nuevoModelo para reconciliar los oyentes activos. Los resultados del comando y los eventos de las suscripciones regresan como nuevos Msg, y el bucle vuelve a empezar.

flowchart TD
Init[init produce Model y Cmd] --> RT[runtime]
RT -->|Model actual| V[view]
V -->|Html Msg| Pantalla[Pantalla]
Pantalla -->|evento Msg| RT
RT -->|Msg y Model| U[update pura]
U -->|Model nuevo y Cmd| RT
RT -->|ejecuta Cmd| Mundo[Mundo exterior]
Mundo -->|resultado Msg| RT
RT -->|reconcilia Sub| Subs[subscriptions]
Subs -->|evento Msg| RT
style RT fill:#cba6f7,color:#11111b
style U fill:#a6e3a1,color:#11111b

Lo decisivo de este dibujo es dónde está cada color. Todo lo verde —update, y por igual view, init y subscriptions— es tuyo y es puro: funciones que transforman valores en valores. Todo lo que ejecuta, espera, pinta y escucha —el nodo malva— es el runtime, y es lo único impuro. La flecha ejecuta Cmd es la única del sistema que toca el mundo, y no la escribes tú.

Núcleo puro, corteza impura, impuesto por el lenguaje

Este reparto tiene un nombre en la disciplina del diseño: núcleo funcional rodeado de una corteza imperativa. La idea circula por muchas comunidades como buena práctica que uno se esfuerza en respetar. Lo singular de Elm es que no la propone como consejo sino que la impone por construcción: el lenguaje, al no darte ninguna función pura que ejecute efectos, hace literalmente imposible mezclar núcleo y corteza. No puedes filtrar impureza a update aunque quieras, porque no existe la herramienta para hacerlo. La separación deja de depender de tu disciplina y pasa a estar garantizada por el sistema de tipos.

-- Browser.element cablea las cuatro piezas puras al runtime
main : Program () Model Msg
main =
    Browser.element
        { init = init                 -- () -> ( Model, Cmd Msg )
        , update = update             -- Msg -> Model -> ( Model, Cmd Msg )
        , view = view                 -- Model -> Html Msg
        , subscriptions = subscriptions  -- Model -> Sub Msg
        }

Browser.element es el punto donde entregas tus cuatro funciones puras al runtime y recibes un Program ejecutable. Fíjate en init: su tipo es () -> ( Model, Cmd Msg ), la misma tupla que update. Esto significa que hasta los efectos iniciales —pedir datos nada más cargar, leer la hora de arranque— se describen igual que cualquier otro: init no ejecuta la primera petición, devuelve su descripción y el runtime la ejecuta. No hay ninguna fase especial de puesta en marcha con reglas distintas; el arranque es solo la primera vuelta del mismo bucle. Browser.document y Browser.application amplían el contrato —control del título, del cuerpo entero, de la navegación por URL— pero conservan exactamente esta estructura de cuatro piezas.

💡
Todo es una vuelta del mismo bucle, incluido el arranque

La uniformidad es la marca de un buen diseño, y aquí es total. El arranque produce ( Model, Cmd ); una interacción produce ( Model, Cmd ); la respuesta de la red produce ( Model, Cmd ); el tic de un reloj produce ( Model, Cmd ). No hay casos especiales: cargar la aplicación, pulsar un botón, recibir datos y escuchar el tiempo son la misma operación vista cuatro veces. Esa ausencia de excepciones al patrón es lo que hace que, una vez entiendes una vuelta del bucle, entiendas el programa entero por grande que sea.

Por qué el runtime puede prometer lo que promete

Que exista un único actor impuro, escrito una vez por el equipo de Elm y compartido por todos los programas, no es solo elegante: es la fuente técnica de las garantías más citadas del lenguaje. La promesa de cero excepciones en tiempo de ejecución sería imposible si tu código pudiera ejecutar efectos, porque la entrada y salida es justo donde nacen los fallos que el compilador no puede prever. Pero como todo efecto pasa por el runtime, este puede envolver cada ejecución, capturar cualquier fallo del mundo y entregártelo como un valor —un Err dentro de un Msg— en lugar de como una excepción que revienta la pila. Un 500 del servidor no lanza nada: llega como un dato que tu update debe manejar. El mismo hecho —el runtime es el dueño de la frontera de efectos— habilita el depurador con viaje en el tiempo, que graba la secuencia de mensajes y la reproduce, y la recarga en caliente que preserva el estado, porque el runtime controla los dos únicos flujos que existen: los mensajes que entran y los comandos que salen.

El runtime es la impureza acorralada en un solo lugar de confianza

La lección de este nivel se vuelve tangible en el runtime. Las lecciones anteriores mostraron cómo convertir efectos y escuchas en valores; esta muestra a dónde van a parar esos valores y por qué el truco funciona. La programación imperativa reparte la impureza por todo el programa: cualquier función, en cualquier línea, puede tocar el mundo, y por eso razonar sobre el conjunto es tan difícil, porque el conjunto entero es potencialmente impuro. Elm hace la jugada contraria y extrema: recoge toda la impureza del universo del programa y la comprime en un único actor, el runtime, que ni siquiera escribes tú. El resultado es una asimetría deliberada y valiosísima. El código que tú produces —tu modelo, tu lógica, tus vistas, tus escuchas— es cien por cien puro y por tanto cien por cien razonable, testeable y rebobinable; y la parte impura, inevitable en cualquier programa útil, no está dispersa ni bajo tu responsabilidad, sino concentrada en una pieza pequeña, cerrada, probada por miles de aplicaciones y compartida por todas. Esto invierte la carga habitual: en vez de confiar en que cada programador respete la frontera entre lo puro y lo impuro, el lenguaje coloca esa frontera en un sitio fijo y no te deja cruzarla. Las garantías de Elm no son promesas de buena voluntad; son teoremas que se siguen de haber acorralado la impureza en un actor único y de confianza. Comprender el runtime es comprender que la pureza de tu código no es un mérito tuyo que puedas estropear, sino una propiedad estructural que el sistema sostiene por ti.

⚔️ Haz visible al actor invisible
  1. Dibuja el bucle del runtime con sus cuatro pasos —guardar modelo, pintar vista, ejecutar comando, reconciliar suscripciones— y marca cuál es el único paso impuro.
  2. Explica por qué init devuelve ( Model, Cmd Msg ) y no solo Model, y da un ejemplo de efecto que quieras lanzar al arrancar.
  3. Rastrea una petición completa a través del runtime: desde el Msg que la dispara hasta el Msg que trae su resultado, nombrando qué hace el runtime en cada tramo.
  4. Argumenta por qué la garantía de cero excepciones en ejecución sería imposible si tu update pudiera ejecutar la red directamente.
  5. Relaciona el patrón núcleo puro con corteza impura con lo que Elm impone y explica en qué se diferencia de adoptarlo como mera convención en otro lenguaje.
  6. Enumera qué le entregas a Browser.element y clasifica cada pieza como pura o impura; comprueba que todo lo que escribes cae del lado puro.