Sub: suscribirse a los eventos que el mundo emite
Un Cmd sirve para efectos que tú inicias una vez, pero hay fuentes de eventos que no inicias y que ocurren en el exterior de forma continua: el reloj avanza, llegan mensajes por un websocket, el usuario teclea, la ventana cambia de tamaño. Para escucharlas Elm añade subscriptions, una función del modelo que declara qué corrientes de eventos externos quieres recibir ahora mismo, cada una mapeada a un Msg. Esta lección desarrolla Sub Msg con Time.every, Browser.Events.onKeyDown y los puertos de entrada; explica que la escucha es declarativa, no imperativa, porque devuelves el conjunto de suscripciones deseado y el runtime lo compara con el anterior y añade o quita listeners reales por ti; y contrasta Cmd como hacer esto una vez frente a Sub como avísame cuando esto pase, dos flujos que terminan igual, como un Msg que reentra por update. La idea de fondo es que qué escuchas es estado derivado del modelo, recomputado en cada actualización.
Un Cmd resuelve la mitad del problema de los efectos: los que tú inicias y que ocurren una vez, como pedir un recurso o tirar un dado. Pero hay otra mitad de naturaleza distinta. Hay cosas que no inicias tú y que suceden en el exterior de forma continua, a su propio ritmo: el reloj que avanza segundo a segundo, los mensajes que llegan por un websocket cuando al servidor le apetece enviarlos, las teclas que el usuario pulsa, la ventana que cambia de tamaño. Frente a esas fuentes no quieres ordenar haz esto una vez, sino declarar avísame cuando esto ocurra. Esa es la función de Sub, las suscripciones. Y del mismo modo que Cmd describe un efecto como un valor, Sub describe una escucha como un valor: no enganchas ni desenganchas oyentes a mano, sino que declaras, en función del modelo actual, qué corrientes de eventos externos te interesan ahora, y dejas que el runtime traduzca esa declaración en oyentes reales.
- Distinguir el efecto puntual que inicias con
Cmdde la corriente continua de eventos que escuchas conSub. - Leer la firma
subscriptions : Model -> Sub Msgy entender por qué depende del modelo. - Declarar suscripciones con
Time.every,Browser.Eventsy puertos, y componerlas conSub.noneySub.batch. - Comprender que la escucha es declarativa: devuelves el conjunto deseado y el runtime reconcilia los oyentes reales.
Dos formas de efecto: iniciar y escuchar
La asimetría entre Cmd y Sub es la de dos direcciones del tiempo. Un Cmd es un impulso que sale de ti hacia el mundo y termina: lo lanzas, se ejecuta, vuelve un resultado, se acabó. Una Sub es una antena orientada hacia el mundo que permanece abierta: mientras la mantengas declarada, cada vez que la fuente emita —cada segundo, cada mensaje, cada tecla— llegará un Msg. Uno es de disparo único y lo inicia tu lógica; la otra es de flujo continuo y la inicia el exterior. Lo que comparten es el destino: ambos terminan convertidos en un Msg que reentra por update, de modo que ni la respuesta de una petición ni el tic de un reloj tienen una vía privilegiada para tocar el modelo. Todo pasa por la misma puerta.
flowchart LR Reloj[Reloj] --> S[subscriptions del Model] Teclado[Teclado] --> S WS[WebSocket] --> S S --> RT[runtime] RT -->|Msg| U[update] U -->|Model nuevo| S style S fill:#89b4fa,color:#11111b style U fill:#a6e3a1,color:#11111b
subscriptions es una función del modelo
Aquí está el detalle que suele sorprender y que encierra toda la potencia del diseño. Las suscripciones no se declaran una vez al arrancar; se declaran en una función subscriptions : Model -> Sub Msg que el runtime vuelve a llamar después de cada actualización del modelo. Es decir, qué escuchas es una función de tu estado actual. Si el modelo dice que un cronómetro está en marcha, devuelves la suscripción al reloj; si dice que está en pausa, devuelves Sub.none, y el reloj deja de entregar tics. No paras el temporizador con una orden imperativa: cambias el modelo, y la suscripción declarada cambia con él.
-- Que escuchas depende del estado: es estado derivado
subscriptions : Model -> Sub Msg
subscriptions model =
case model.estado of
EnMarcha ->
Sub.batch
[ Time.every 1000 Tic
, Browser.Events.onKeyDown teclaDecodificador
]
Pausado ->
-- sin suscripciones: el reloj deja de emitir
Sub.none
Time.every 1000 Tic declara que quieres un mensaje Tic cada mil milisegundos. Browser.Events.onKeyDown declara que quieres un mensaje por cada tecla pulsada, decodificado del evento del navegador. Sub.batch combina varias suscripciones en una, igual que Cmd.batch hacía con los comandos, y Sub.none es la ausencia de escucha. Un puerto de entrada, port recibir : (Valor -> msg) -> Sub msg, es la vía por la que un websocket o cualquier código JavaScript externo empuja eventos hacia Elm, y también produce un Sub.
Time.every
Declara un Msg periódico cada tantos milisegundos. Devolverla o no según el modelo es cómo se arranca y se para un reloj en Elm, sin temporizadores imperativos.
Browser.Events
Escucha eventos globales del navegador —teclado, ratón, redimensionado, foco— decodificando el evento crudo en un Msg con tus propios decodificadores.
Puertos de entrada
La frontera con JavaScript: un websocket, una API del navegador sin binding en Elm o cualquier fuente externa empuja valores que llegan como un Sub.
Sub.none y Sub.batch
Componer la escucha como un valor: nada, una fuente o varias combinadas, elegidas en función del estado actual del modelo.
Escucha declarativa: describir el conjunto, no gestionar oyentes
La consecuencia más profunda de que subscriptions sea una función del modelo es que la gestión de oyentes se vuelve declarativa. En una API imperativa clásica añades un oyente con una llamada y lo quitas con otra, y toda la corrección depende de que cada alta tenga su baja: si olvidas una, tienes una fuga de memoria o un oyente zombi que sigue disparando. Elm elimina esa clase entera de errores. Tú nunca añades ni quitas oyentes; solo describes, para el modelo actual, el conjunto de suscripciones que deberían estar activas. Después de cada update, el runtime llama de nuevo a subscriptions, compara el conjunto nuevo con el anterior y hace por ti las altas y bajas reales: activa las que aparecen, cancela las que desaparecen, deja las que siguen.
Esto es la misma idea que el DOM virtual, trasladada a los oyentes de eventos. Con la vista no describes qué nodos crear o destruir, describes qué HTML debería existir para este modelo y el runtime calcula el diff. Con las suscripciones no describes qué oyentes enganchar o soltar, describes qué escuchas debería haber para este modelo y el runtime calcula el diff. En ambos casos declaras un objetivo en función del estado y delegas la transición imperativa. Por eso en Elm no existe el bug de la suscripción que se quedó viva tras cambiar de pantalla: si el modelo ya no la declara, el runtime ya la canceló.
Piensa en un chat sobre websocket. Mientras el modelo diga que hay una sala abierta, subscriptions devuelve la escucha del puerto correspondiente; en cuanto el usuario sale y el modelo lo refleja, la función deja de devolverla y el runtime cierra la escucha. No hay un cerrarConexion disperso por los manejadores de eventos: hay un único lugar, subscriptions, que traduce sin ambigüedad el estado a un conjunto de escuchas. La corrección deja de depender de recordar emparejar altas y bajas y pasa a depender de una sola función pura del modelo, que puedes leer entera y razonar de un vistazo.
El giro conceptual de Sub es tan radical como el de Cmd, pero apunta a otra dimensión. Cmd convirtió el efecto de salida en un valor; Sub convierte la escucha de entrada en estado derivado. En el modelo mental imperativo, suscribirse es un acto puntual con memoria oculta: haces una llamada, en algún registro invisible queda anotado que estás escuchando, y para dejar de hacerlo debes ejecutar el acto inverso sobre ese registro que no ves. Toda la fragilidad de los oyentes nace de esa memoria oculta y de la obligación de mantenerla coherente a mano. Elm la elimina de raíz al declarar que el conjunto de suscripciones activas no es un estado independiente que tú administras, sino una función pura del modelo, recalculada entera después de cada mensaje. Escuchar deja de ser algo que haces y pasa a ser algo que tu estado implica. La pregunta arranca este reloj se disuelve en la pregunta el modelo, tal como está ahora, implica que el reloj deba estar sonando; y si la respuesta cambia porque el modelo cambió, el runtime ajusta la realidad para que coincida con la declaración. Es la misma filosofía que recorre toda la Arquitectura Elm llevada hasta su conclusión: no describes transiciones del mundo, describes cómo debería ser el mundo dado tu estado, y delegas en un intérprete la tarea sucia de llevarlo hasta ahí.
- Escribe
subscriptions : Model -> Sub Msgpara un cronómetro que devuelvaTime.every 1000 Ticcuando el modelo esté en marcha ySub.nonecuando esté en pausa. - Comprueba que pausar y reanudar el cronómetro no requiere ninguna orden sobre temporizadores: basta cambiar el modelo para que el conjunto de suscripciones cambie.
- Añade con
Sub.batchuna segunda suscripción aBrowser.Events.onKeyDownque permita reiniciar con una tecla, y decodifica el evento en unMsg. - Explica, con el ejemplo de un websocket, por qué en Elm no puede existir el bug del oyente que sigue vivo tras cerrar la conexión.
- Contrasta en una tabla
CmdySubsegún quién inicia el efecto, si es puntual o continuo, y por qué puerta vuelve el resultado. - Argumenta por qué que
subscriptionssea una función del modelo convierte la gestión de oyentes en un problema declarativo en vez de imperativo.