wandres.dev
ELM: EFECTOS · commands y subscriptions

Cmd: describir un efecto como un valor que update devuelve

La respuesta de Elm al problema de los efectos es cambiar la firma de update para que devuelva una tupla con el modelo nuevo y un Cmd Msg, un valor inerte que describe un efecto sin ejecutarlo. Esta lección desarrolla esa inversión: construir un Cmd con Http.get, Random.generate o Task.perform no dispara nada, solo produce una receta que update entrega al runtime; el efecto se ejecuta fuera y su resultado vuelve por la misma puerta que cualquier otro evento, convertido en un Msg que expect decodifica. Se cubren Cmd.none para no hacer nada, Cmd.batch para lanzar varios a la vez, y el patrón de que la vista despacha un mensaje, update devuelve modelo mas comando, y la respuesta reentra como mensaje. La idea central es que el efecto se reifica: deja de ser una acción que la función realiza y pasa a ser un dato que la función retorna, preservando intacta la pureza de la lógica.

⏱ 18 min

La solución de Elm al problema de la lección anterior es de una economía asombrosa: no cambia la naturaleza de update, solo cambia lo que devuelve. En lugar de retornar únicamente el modelo nuevo, update retorna una tupla ( Model, Cmd Msg ). Ese segundo elemento, el Cmd Msg, es el corazón de todo el nivel: es un valor —tan inerte y ordinario como un número o una lista— que describe un efecto sin ejecutarlo. Construir un Cmd no dispara ninguna petición, no toca ningún reloj, no abre ningún socket; solo fabrica una receta. update decide qué efecto quiere, lo empaqueta en ese valor y se lo entrega al runtime, que será quien lo ejecute. Así la función sigue siendo perfectamente pura —dado un mensaje y un modelo produce siempre la misma tupla— y, sin embargo, la aplicación adquiere la capacidad de hablar con el mundo. El efecto ha dejado de ser algo que se hace para convertirse en algo que se devuelve.

🎯 Al terminar esta lección sabrás
  • Leer la firma real update : Msg -> Model -> ( Model, Cmd Msg ) y entender qué añade el segundo elemento.
  • Comprender que un Cmd Msg es un valor descriptivo e inerte: construirlo no ejecuta nada.
  • Construir comandos con Http.get y expect, y componerlos con Cmd.none y Cmd.batch.
  • Trazar el viaje completo: la vista despacha un Msg, update devuelve un Cmd, y el resultado reentra como otro Msg.

La firma que lo cambia todo

La update madura de Elm tiene una firma que resuelve el dilema sin sacrificar la pureza. Devuelve dos cosas emparejadas: el estado siguiente y una descripción de qué efectos deben ejecutarse a continuación. El tipo Cmd Msg se lee así: un comando que, cuando el runtime lo ejecute y obtenga un resultado, lo entregará de vuelta en forma de un valor de tipo Msg. El parámetro de tipo no es decorativo: dice por qué puerta volverá la respuesta.

-- La firma real, y una rama que decide pedir sin ejecutar la peticion
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PulsarCargar ->
            ( { model | estado = Cargando }
            , Http.get
                { url = "/api/chiste"
                , expect = Http.expectJson RecibirChiste decodChiste
                }
            )

        RecibirChiste (Ok texto) ->
            ( { model | estado = Listo texto }, Cmd.none )

        RecibirChiste (Err _) ->
            ( { model | estado = Fallo }, Cmd.none )

Observa lo que ocurre y lo que no. En la rama PulsarCargar, Http.get no descarga nada: construye un valor Cmd Msg y lo coloca en la tupla. update termina, pura, habiendo decidido dos cosas —el modelo pasa a Cargando y hay que ejecutar esta petición— sin haber ejecutado ninguna. El campo expect es la clave del regreso: declara que la respuesta se decodificará y se envolverá en el mensaje RecibirChiste, que reentrará por update como cualquier otro evento. Las ramas que reciben ese mensaje ya no necesitan efecto y lo dicen con Cmd.none.

Un comando es un valor inerte

Conviene insistir en esto porque contradice el hábito imperativo: en Elm, Http.get { ... } no es una llamada que hace algo, es una expresión que produce un dato. Es la diferencia entre escribir una orden en un papel y ejecutarla. El papel —el Cmd— puede pasarse, ignorarse, combinarse con otros o descartarse, y hasta que el runtime no lo lea y actúe, el mundo no cambia. Esta cualidad de valor de primera clase es lo que permite componer efectos con las mismas herramientas con que se componen los datos.

🚫

Cmd.none

El comando que no hace nada. Se devuelve cuando una transición de estado no necesita ningún efecto, y mantiene homogénea la firma: update siempre retorna una tupla.

📦

Cmd.batch

Agrupa una lista de comandos en uno solo para lanzar varios efectos desde una misma rama; el runtime los ejecuta y cada resultado vuelve por su propio Msg.

🎁

expect y Msg

El campo expect fija cómo se interpreta la respuesta y en qué mensaje viaja de vuelta; es el conector entre el mundo impuro y tu bucle puro de mensajes.

🧪

Pureza intacta

Como el Cmd es solo un valor, puedes afirmar en una prueba que update devolvió el comando esperado sin ejecutar ninguna red: inspeccionas la receta, no el efecto.

El mismo patrón cubre todos los efectos, no solo la red. Random.generate GotDado (Random.int 1 6) produce un Cmd Msg que, al ejecutarse, entregará un dado en el mensaje GotDado. Task.perform y Task.attempt convierten tareas en comandos. Un puerto de salida devuelve un Cmd. En todos los casos la mecánica es idéntica: update fabrica la descripción y la retorna; nunca la ejecuta.

💡
La respuesta entra por la misma puerta que todo lo demás

No hay en Elm un mecanismo especial de callbacks para la asincronía. El resultado de un comando no llega a una función aparte ni a una promesa: reentra por update como un Msg corriente, indistinguible en su trato de un clic o de una pulsación de tecla. Esto es lo que mantiene el flujo unidireccional aun con efectos: haya lo que haya ocurrido en el mundo, la única forma de que afecte a tu estado es convertirse en un mensaje que pasa por la única puerta que actualiza el modelo. La asincronía no abre una segunda vía; se disuelve en el mismo bucle.

El viaje de ida y vuelta

Poner las piezas en movimiento revela el ciclo. La vista, al pulsar un botón, despacha PulsarCargar. El runtime llama a update, que devuelve el modelo en Cargando y un Cmd que describe la petición. El runtime almacena el modelo, vuelve a pintar la vista —que ahora muestra un indicador de carga— y ejecuta el comando de verdad, hablando con la red. Cuando la respuesta llega, el runtime la envuelve, según lo pedido por expect, en el mensaje RecibirChiste y lo despacha, cerrando el círculo: update corre otra vez, ahora con el dato en mano, y decide el estado Listo sin más efecto.

flowchart LR
Vista[Vista] -->|PulsarCargar| U[update pura]
U -->|Model y Cmd Msg| RT[runtime]
RT -->|ejecuta el efecto| Red[Servidor]
Red -->|respuesta| RT
RT -->|RecibirChiste| U
U -->|Model nuevo| Vista
style U fill:#a6e3a1,color:#11111b
style RT fill:#cba6f7,color:#11111b

Lo notable es que en ninguna parte de tu código aparece la ejecución. Tú describes; el runtime ejecuta. Esa división del trabajo es la que hace que puedas escribir una aplicación entera que consulta APIs, tira dados y persiste datos sin escribir jamás una línea impura: toda la impureza vive en el runtime, y tu programa se comunica con él exclusivamente a través de valores —comandos que salen, mensajes que entran—.

Reificar el efecto es convertir un verbo en un sustantivo

Lo que hace Cmd es una operación conceptual que merece nombre propio: reificar, convertir en cosa lo que era acción. En la mayoría de los lenguajes, hacer una petición es un verbo —una instrucción que se ejecuta en el instante en que se escribe— y por eso contamina cualquier función que la contenga. Elm transforma ese verbo en un sustantivo: la petición ya no es algo que ocurre, es un objeto que existe, un valor que puedes construir, nombrar, guardar en una variable, meter en una lista, devolver desde una función pura y pasar de mano en mano sin que suceda nada. Y como es un valor, hereda todo lo que sabemos hacer con los valores: componerlos con Cmd.batch, sustituirlos por Cmd.none, compararlos en una prueba, inspeccionarlos en un depurador. La ejecución se pospone hasta un único punto —el runtime— y se separa por completo de la decisión. Esta es la misma jugada que hacen los datos frente al código en tantos lugares profundos de la computación: una descripción es más manejable que una acción, porque la puedes examinar antes de que sea irreversible. update no hace la petición; escribe una nota que dice hágase esta petición, y esa diferencia mínima en la redacción es la que preserva la pureza de todo el núcleo mientras la aplicación, por fin, toca el mundo.

⚔️ Fabrica comandos sin ejecutarlos
  1. Cambia la firma de una update de contador a ( Model, Cmd Msg ) y comprueba que cada rama debe devolver ahora una tupla, usando Cmd.none donde no haya efecto.
  2. Escribe una rama que, ante un mensaje PulsarCargar, construya un Http.get con expect = Http.expectJson y ponga el modelo en un estado de carga sin ejecutar nada.
  3. Añade las dos ramas del regreso —Ok y Err— que reciben el Msg con el resultado y actualizan el modelo, cada una con Cmd.none.
  4. Usa Cmd.batch para lanzar dos efectos desde una misma rama y razona por qué cada resultado vuelve en su propio mensaje y no en uno combinado.
  5. Escribe una prueba que aplique PulsarCargar a update y afirme sobre el Cmd devuelto sin ejecutar ninguna petición; explica qué hace posible esa prueba.
  6. Sustituye la red por Random.generate y observa que la mecánica —describir un comando, recibir el resultado como Msg— es idéntica pese a ser otro efecto.