La lección universal: el efecto como un dato que un runtime ejecuta
Cerramos el nivel elevando su idea por encima de Elm: describir un efecto como un valor inmutable que un runtime ejecuta —en lugar de ejecutarlo tú, en línea, dentro de tu lógica— es la contribución más influyente de la Arquitectura Elm, la que saltó la valla del lenguaje y reapareció con otros nombres en arquitecturas que jamás escribieron Elm. Esta lección formula el patrón en su forma general —una lógica pura decide y devuelve una descripción del efecto; un intérprete ajeno la realiza y devuelve el resultado como un mensaje— y rastrea su préstamo en tres lugares: el tipo Effect de The Composable Architecture en Swift, los objetos planos que un generador emite con yield en redux-saga, y el loop con Cmd de redux-loop, que porta casi al pie de la letra la tupla modelo más comando de Elm a los reducers de Redux. Explica por qué reificar el efecto —convertir el verbo ejecutar en el sustantivo descripción— es lo que vuelve la lógica pura, testeable sin simulacros y determinista, y por qué esa robustez explica que la idea se reinvente una y otra vez; contrasta el patrón con el thunk, que difiere la ejecución pero sigue ejecutando por dentro, para afilar qué cuenta y qué no cuenta como efecto como dato.
A lo largo de este nivel hemos visto a Elm hacer una y otra vez el mismo gesto: tomar algo que en otros lenguajes es una acción —pedir a la red, leer el reloj, escuchar el teclado— y convertirlo en un valor. Un Cmd no ejecuta una petición, la describe; una Sub no engancha un oyente, lo declara; y el runtime, un actor ajeno a tu código, es el único que ejecuta de verdad. Conviene ahora subir un peldaño y ver esa idea desnuda, separada de la sintaxis de Elm, porque es mucho más grande que Elm. La idea es esta: un efecto puede representarse como un dato inmutable, y quien lo ejecuta no eres tú sino un intérprete que recibe esa descripción. Formulada así, no pertenece a ningún lenguaje. Es tan portátil que ha saltado la valla de Elm y ha reaparecido, con otros nombres y en otras comunidades, en arquitecturas escritas por gente que quizá nunca compiló una línea de Elm. Esta última lección del nivel trata de esa migración: de por qué la idea es tan robusta que se reinventa sola, y de qué revela sobre la computación el hecho de que describir sea casi siempre mejor que hacer.
- Formular la idea universal despojada de Elm: un efecto es un valor inmutable que describe trabajo, y un runtime ajeno a tu lógica lo ejecuta.
- Reconocer el mismo patrón bajo otros nombres: el tipo
Effectde TCA, los objetos que un generador emite conyielden redux-saga y elloopconCmdde redux-loop. - Explicar por qué reificar el efecto —convertir un verbo en un sustantivo— hace la lógica pura, testeable sin simulacros y determinista.
- Distinguir con nitidez describir un efecto de ejecutarlo, usando el
thunkcomo contraejemplo que difiere pero no reifica.
Una idea que salta la valla del lenguaje
Reduzcamos el nivel entero a una sola frase y quitémosle la ropa de Elm. Tu lógica de estado es una función pura que, ante un evento, decide dos cosas: cómo queda el estado y qué efectos deberían ocurrir. Lo segundo no lo ejecuta; lo devuelve, empaquetado en un valor. Ese valor es una receta inerte —se puede guardar, pasar, comparar, combinar, descartar— y hay un único lugar, el runtime, que la lee y la lleva a cabo. El resultado del efecto no regresa por una vía privilegiada: reentra como un evento más, por la misma puerta que todo lo demás.
-- El gesto de Elm, reducido a su esencia: decidir devuelve una descripcion
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
PulsarCargar ->
-- no se ejecuta ninguna peticion aqui: se devuelve su descripcion
( { model | estado = Cargando }, pedirUsuario )
El Cmd de la derecha es el dato. update lo fabrica y lo entrega; no lo ejecuta. Cambia el nombre Cmd por otro cualquiera y el patrón sigue intacto: una función pura que produce una representación del efecto, y un intérprete que la consume al borde del sistema. Eso es exactamente lo que hicieron otros, en otros lenguajes, sin pedir permiso.
Y conviene notar que el nivel te mostró la idea dos veces, no una. Un Cmd reifica el efecto de salida —lo que tú inicias— y una Sub reifica la escucha de entrada —lo que te llega—; ambos son el mismo movimiento aplicado a las dos direcciones del tiempo. Cuando un diseño repite el mismo gesto en contextos distintos, deja de parecer un truco puntual y empieza a delatar un principio. El principio es que la frontera entre decidir y obrar puede materializarse en un valor que la cruza.
Una buena prueba de que entiendes una idea es despojarla de su vocabulario. Tapa la palabra Cmd y describe lo que hace: un valor, producido por una función pura, que representa un efecto que otro ejecutará. Esa descripción no menciona Elm por ninguna parte. Por eso encaja igual de bien en un Effect, en un objeto emitido con yield o en un loop: no estabas aprendiendo una API de Elm, estabas aprendiendo una forma de partir un programa en dos mitades, la que decide y la que obra.
El mismo patrón con otros nombres
Elm · Cmd Msg
El original. update devuelve ( Model, Cmd Msg ): el comando es un valor inerte que el runtime ejecuta, y el resultado vuelve como un Msg.
TCA · Effect
En The Composable Architecture (Swift), el reducer muta el estado y devuelve un Effect; el Store lo ejecuta y reinyecta las acciones que produzca.
redux-saga · yield
Cada yield de una saga no ejecuta nada: emite un objeto plano que describe el efecto, y el middleware es quien lo interpreta.
redux-loop · loop
Porta la tupla de Elm a Redux: un reducer devuelve loop(estado, cmd) y un enhancer del store desempaqueta el cmd y lo ejecuta.
En The Composable Architecture, la biblioteca de Point-Free para Swift, el reducer no devuelve solo el estado: devuelve un Effect. Un Effect es un tipo de valor que describe trabajo asíncrono —una llamada de red, un temporizador, una tarea— sin ejecutarlo; el Store es quien lo realiza y reinyecta las acciones resultantes por el mismo reducer. La firma es la tupla de Elm con acento Swift: mutas el estado y devuelves una descripción de efectos, y Effect.none es el gemelo exacto de Cmd.none. El dividendo de esa decisión se cobra en las pruebas con un rigor casi contable: el TestStore de TCA obliga a declarar, paso a paso, qué acciones esperas y qué efectos deberían seguir vivos, y falla si al terminar queda un efecto en vuelo que no afirmaste. Solo se puede exigir tanto porque los efectos son valores que el store puede enumerar y contrastar, no acciones ya disueltas en el mundo.
redux-saga lleva la idea a un extremo especialmente didáctico. Una saga es un generador, y cada yield no ejecuta un efecto: produce un objeto plano de JavaScript que lo describe. El efecto call(api.fetchUsuario, id) no llama a la API; devuelve un objeto —un registro con el tipo del efecto, la función y sus argumentos— que el middleware interpretará más tarde.
// redux-saga: yield produce una descripcion, no una ejecucion
function* cargarUsuario(id: string) {
// call(...) NO invoca la API: devuelve un objeto que describe la llamada
const usuario = yield call(api.fetchUsuario, id)
yield put({ type: "USUARIO_OK", usuario })
}
La prueba de una saga es la demostración más limpia de por qué esto importa: recorres el generador con next() y afirmas que el valor emitido es igual a call(api.fetchUsuario, id) —comparas dos objetos, dos descripciones— sin tocar jamás la red. Verificas la intención, no el efecto. Y redux-loop es el préstamo más literal de todos: reintroduce la tupla de Elm en Redux. Un reducer, que en Redux ortodoxo solo puede devolver estado, pasa a poder devolver loop(estado, cmd), donde cmd es un valor construido con Cmd.run que describe un efecto. Un enhancer del store lo desempaqueta, lo ejecuta y despacha de vuelta las acciones que produzca. Es, casi carácter por carácter, ( Model, Cmd Msg ) trasplantado a JavaScript.
// redux-loop: el reducer devuelve estado y una descripcion de efecto
function usuarios(state = inicial, action) {
switch (action.type) {
case "CARGAR":
// loop(...) no ejecuta nada: empaqueta estado mas Cmd como un valor
return loop(
{ ...state, estado: "cargando" },
Cmd.run(api.fetchUsuario, {
successActionCreator: usuarioOk,
failActionCreator: usuarioError,
args: [action.id],
})
)
default:
return state
}
}
El reducer sigue siendo puro —devuelve un valor y nada más— y el Cmd.run es la receta que el enhancer del store ejecutará por su cuenta, despachando usuarioOk o usuarioError según lo que devuelva la red. Cámbiale los nombres y esa rama CARGAR es, línea por línea, la rama PulsarCargar de Elm con la que abrimos la lección.
flowchart TD Elm[Elm Cmd Msg] --> Idea[Efecto como dato] TCA[TCA Effect] --> Idea Saga[redux-saga objeto yield] --> Idea Loop[redux-loop loop y Cmd] --> Idea Idea --> RT[Un interprete lo ejecuta y devuelve un mensaje] style Idea fill:#f9e2af,color:#11111b style RT fill:#cba6f7,color:#11111b
Los cuatro difieren en el sabor —tipos estáticos en Elm y Swift, objetos dinámicos en JavaScript, un generador que se lee como código secuencial en saga— pero comparten la espina dorsal: en ninguno de ellos la función que decide es la función que ejecuta. Esa coincidencia entre comunidades que apenas se hablan entre sí es la mejor evidencia de que la idea no era un capricho local de Elm, sino algo que estaba esperando a ser descubierto varias veces.
Conviene ver qué no es efecto como dato. Un thunk de Redux es una función (dispatch, getState) => ... que el middleware llama, y dentro de ella ejecutas la red directamente. El thunk difiere la ejecución —no corre hasta que alguien lo invoca— pero no la reifica: por dentro sigue siendo código imperativo que toca el mundo, opaco a la inspección. No puedes comparar dos thunks para ver si describen el mismo efecto, y para probarlo debes simular dispatch y el mundo entero. Diferir no es describir. La distancia entre un thunk y un Cmd es la distancia entre una función que hará algo y un valor que dice qué hacer.
Por qué la idea es tan robusta
El dibujo es el mismo trácese en Elm, en Swift o en JavaScript: una lógica pura decide y devuelve un valor, un intérprete lo ejecuta contra el mundo, y el resultado vuelve como un mensaje que reentra por la lógica. De esa forma cuelgan tres propiedades que ninguna arquitectura basada en ejecución en línea puede ofrecer a la vez.
flowchart LR Logica[Logica pura] -->|devuelve| Efecto[Efecto como valor] Efecto --> RT[Runtime interprete] RT -->|ejecuta| Mundo[Mundo exterior] Mundo -->|resultado como mensaje| Logica style Logica fill:#a6e3a1,color:#11111b style Efecto fill:#f9e2af,color:#11111b style RT fill:#cba6f7,color:#11111b
La primera es la pureza. La lógica no realiza el efecto, lo devuelve; por tanto sigue siendo una función de entradas a salidas, sin dependencias ocultas del mundo. Toda la impureza que el programa necesita —y alguna necesita, o no serviría para nada— queda acorralada en el intérprete, un único lugar pequeño y ajeno a tu lógica, en vez de repartida por cada rama que toca la red.
La segunda es la testabilidad sin simulacros. Como el efecto es un valor, la prueba compara valores: se afirma que la lógica devolvió la descripción esperada, sin simular la red ni el reloj, porque no se ejecuta nada. Se inspecciona la receta, no el plato. La prueba de la saga —recorrer el generador y comparar el objeto emitido— y la del update de Elm —aplicar un mensaje y comparar el Cmd devuelto— son, en el fondo, la misma prueba escrita en dos idiomas distintos.
La tercera es el determinismo y el rebobinado. La misma entrada produce siempre la misma descripción de efecto, y como la secuencia de decisiones es una secuencia de datos, se puede grabar, reproducir y recorrer hacia atrás. El depurador con viaje en el tiempo de Elm y las herramientas de Redux no son florituras: existen porque el historial es data —una lista de valores que se puede volver a aplicar desde el principio— y no una estela de acciones ya consumidas e irrepetibles.
El criterio que te llevas, y su precio
Sería una mala lección universal la que solo cantara las ventajas. Reificar el efecto tiene un coste, y nombrarlo es lo que convierte la idea en criterio y no en dogma. El precio es la indirección: aparece una pieza nueva —el runtime, el middleware, el enhancer, el Store— que hay que construir o adoptar, y un salto mental extra entre el punto donde se decide el efecto y el punto donde de verdad ocurre. En un guion de veinte líneas que hace un fetch y termina, esa indirección es pura ceremonia y no se paga sola; ahí ejecutar el efecto en línea es lo correcto y punto.
El criterio, por tanto, es proporcional. El valor de describir en vez de hacer crece exactamente con cuánto necesites razonar, probar, reproducir o deshacer lo que el programa hace con el mundo. Una aplicación con lógica de estado rica, muchos efectos entrelazados y una exigencia real de pruebas fiables es el terreno donde la reificación se devuelve con creces; un script efímero, no. Por eso la idea madura no dice reifica siempre, sino algo más fino: aprende a reconocer cuándo el efecto en mitad de tu lógica te está costando pureza, y ten a mano la maniobra de convertirlo en un valor que otro ejecute al borde.
Nótese, además, que la simetría se cierra por el otro lado. No solo la salida se vuelve dato: la entrada también. El resultado del efecto no regresa por un callback privilegiado ni por una promesa colgada de la nada, sino como un mensaje —otro valor— que reentra por la misma puerta que cualquier evento. Describes el efecto como dato al salir y recibes su resultado como dato al volver, y entre esos dos datos vive un intérprete que es el único que conoce el mundo. Esa clausura —valores que salen, valores que entran, impureza confinada en medio— es lo que hace que el bucle entero sea inspeccionable de principio a fin.
La forma adulta de sostener esta idea es como criterio graduado, no como mandamiento. Consiste en hacerse siempre la misma pregunta —este efecto que voy a ejecutar aquí, ¿me conviene devolverlo como una descripción para que otro lo realice?— y dejar que la respuesta dependa de cuánta pureza, testabilidad y trazabilidad estés dispuesto a comprar al precio de una indirección. En una arquitectura como Elm o TCA la respuesta es sí por defecto, porque el lenguaje y la biblioteca ya te pusieron el intérprete; en un rincón imperativo pequeño y aislado, quizá no. Lo que no debería volver a ocurrirte, después de este nivel, es ejecutar un efecto en mitad de tu lógica sin haberte hecho la pregunta.
La lección de este nivel, llevada a su forma más general, es una de las más profundas de toda la computación, y no la inventó Elm. Representar una acción como un dato es un motivo que reaparece por todas partes: el patrón Command de la orientación a objetos, las free monads de la programación funcional, un árbol de sintaxis frente al código fuente ejecutándose, una consulta SQL como valor frente a un bucle a mano, un objeto petición frente a una escritura directa en el socket, la representación intermedia de un compilador frente a la máquina ejecutando. En todos esos casos alguien decidió colocar, entre la decisión y la ejecución, una representación inspeccionable de lo que va a ocurrir. Lo que hizo Elm no fue inventar la idea, sino algo casi tan valioso: convertirla en la columna vertebral de una arquitectura de aplicaciones entera y demostrar que era ergonómica, que se podía construir un programa real, grande y agradable de escribir sobre ese único principio. Por eso se copió sin disimulo. La razón última de que describir gane a hacer es de una simplicidad desarmante: un dato lo puedes examinar antes de que sea irreversible, y una acción solo la puedes ejecutar y esperar. Un efecto descrito se puede registrar, comparar, combinar, cancelar, reproducir, probar y hasta enviar por un cable; un efecto ejecutado desaparece en el instante en que ocurre y no deja más que su consecuencia. Poner una representación entre el decidir y el obrar es lo que vuelve posible todo lo que queremos hacerle a un programa: razonarlo, testearlo, optimizarlo, deshacerlo, distribuirlo. Y de ahí sale el consejo transferible que te llevas del nivel entero, que no es usa Cmd: cada vez que te descubras ejecutando un efecto en mitad de tu lógica, pregúntate si podrías, en su lugar, devolver un valor que lo describa y entregar ese valor a un único intérprete situado en el borde del sistema. Si puedes, toda la impureza se desploma en un solo lugar y el resto de tu programa se vuelve puro, testeable y determinista por construcción. Funcionó en Elm, funcionó en Swift con TCA, funcionó en JavaScript con saga y con loop, y funcionará en un lenguaje que todavía no existe, porque no es una técnica de Elm: es una verdad sobre la diferencia entre un verbo y un sustantivo.
- Escribe en una sola frase, sin nombrar Elm ni ninguna biblioteca, la idea universal del nivel; comprueba que tu frase describe por igual a
Cmd, aEffecty a los objetos emitidos conyield. - Toma una saga mínima que llame a una API y escribe su prueba recorriendo el generador y afirmando sobre el objeto que emite
call, sin simular la red; explica qué hace posible esa prueba. - Contrasta un
thunkde Redux con unCmdde redux-loop: señala cuál reifica el efecto y cuál solo lo difiere, y qué consecuencia tiene cada uno para el testeo. - Dibuja el bucle universal —lógica pura, efecto como valor, runtime, mundo, mensaje de vuelta— y rotula, para Elm, TCA y redux-saga, cómo se llama cada casilla.
- Argumenta por qué el determinismo y el viaje en el tiempo son corolarios de que las decisiones sean datos, y no propiedades que puedas añadir a posteriori a una arquitectura basada en ejecución en línea.
- Encuentra en tu propio código un lugar donde ejecutes un efecto dentro de la lógica y reescríbelo para que la lógica devuelva una descripción que un intérprete realice al borde; anota qué se volvió testeable al hacerlo.