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.
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.
- Leer la firma real
update : Msg -> Model -> ( Model, Cmd Msg )y entender qué añade el segundo elemento. - Comprender que un
Cmd Msges un valor descriptivo e inerte: construirlo no ejecuta nada. - Construir comandos con
Http.getyexpect, y componerlos conCmd.noneyCmd.batch. - Trazar el viaje completo: la vista despacha un
Msg,updatedevuelve unCmd, y el resultado reentra como otroMsg.
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.
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—.
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.
- Cambia la firma de una
updatede contador a( Model, Cmd Msg )y comprueba que cada rama debe devolver ahora una tupla, usandoCmd.nonedonde no haya efecto. - Escribe una rama que, ante un mensaje
PulsarCargar, construya unHttp.getconexpect = Http.expectJsony ponga el modelo en un estado de carga sin ejecutar nada. - Añade las dos ramas del regreso —
OkyErr— que reciben elMsgcon el resultado y actualizan el modelo, cada una conCmd.none. - Usa
Cmd.batchpara lanzar dos efectos desde una misma rama y razona por qué cada resultado vuelve en su propio mensaje y no en uno combinado. - Escribe una prueba que aplique
PulsarCargaraupdatey afirme sobre elCmddevuelto sin ejecutar ninguna petición; explica qué hace posible esa prueba. - Sustituye la red por
Random.generatey observa que la mecánica —describir un comando, recibir el resultado comoMsg— es idéntica pese a ser otro efecto.