wandres.dev
ELM: EFECTOS · commands y subscriptions

El problema: una función pura no puede tocar el mundo

La Arquitectura Elm descansa sobre una afirmación con filo: update es una función pura que recibe un mensaje y un modelo y devuelve un modelo nuevo, sin más. Pero ninguna aplicación real vive sin pedir datos a un servidor, leer la hora, generar azar o escribir en localStorage, y todo eso son efectos, justo lo único que una función pura no puede realizar. Esta lección instala la tensión fundadora del nivel: define pureza y transparencia referencial y por qué update las exige, muestra cómo una petición HTTP o una lectura del reloj rompen ambas propiedades a la vez, y descarta las dos salidas ingenuas —volver impura a update o esconder el efecto en estado mutable global—. Termina reformulando el dilema no como cómo suprimir el efecto, sino como una cuestión de frontera: quién decide que ocurra y quién lo ejecuta, dos cosas que resultan separables.

⏱ 16 min

La Arquitectura Elm descansa sobre una afirmación tan simple que es fácil no ver su filo: update es una función pura. Recibe un mensaje y un modelo, y devuelve un modelo nuevo; nada más. No escribe en disco, no consulta un reloj, no abre un socket. Y ahí aparece la grieta, porque ninguna aplicación real vive sin hacer exactamente eso: pedir datos a un servidor, saber qué hora es, generar un número al azar, guardar en localStorage. Todas esas operaciones son efectos, y un efecto es, por definición, lo único que una función pura no puede realizar. Este nivel entero nace de esa tensión. Antes de ver la solución de Elm conviene sentir el problema en toda su gravedad: no es un detalle de API, es una contradicción aparente entre la disciplina que da a Elm sus garantías y el trabajo sucio que cualquier programa debe hacer para ser útil.

🎯 Al terminar esta lección sabrás
  • Definir pureza y transparencia referencial, y por qué update las necesita para sostener las garantías de Elm.
  • Mostrar por qué una petición HTTP o una lectura del reloj rompen ambas propiedades a la vez.
  • Descartar las dos salidas ingenuas: volver impura a update o esconder el efecto en estado mutable global.
  • Reformular el dilema como una cuestión de frontera: no cómo suprimir el efecto, sino quién y cuándo lo ejecuta.

La promesa de una update pura

Una función es pura cuando cumple dos condiciones que se refuerzan. La primera es el determinismo: para los mismos argumentos devuelve siempre el mismo resultado. La segunda es la ausencia de efectos observables: no altera nada fuera de sí misma ni depende de nada que no le hayan pasado explícitamente. Juntas dan la transparencia referencial, la propiedad de que cualquier llamada puede sustituirse por su resultado sin cambiar el significado del programa. En su forma introductoria, update es el ejemplo canónico de esta disciplina.

-- La forma con la que se suele presentar TEA por primera vez
update : Msg -> Model -> Model
update msg model =
    case msg of
        Incrementar ->
            { model | conteo = model.conteo + 1 }

        Reiniciar ->
            { model | conteo = 0 }

Cada rama es una expresión que solo mira sus argumentos y produce un valor nuevo; no muta el model que recibe, lo reemplaza. Esa pureza no es purismo académico: es la fuente concreta de las garantías que hacen a Elm lo que es. Porque update es una función de ( Msg, Model ) a Model, puedes probarla sin arrancar un navegador ni simular una red, reproducir cualquier historia de la aplicación aplicando la lista de mensajes desde el estado inicial, y confiar en que no existe un camino oculto por el que el estado cambie a tus espaldas. El depurador con viaje en el tiempo y la ausencia de excepciones en tiempo de ejecución no son magia: son corolarios de que la lógica de estado es una función pura.

Por qué un efecto rompe esa promesa

Tomemos el efecto más común, una petición HTTP, y veamos que viola las dos condiciones a la vez. Rompe el determinismo porque su resultado no depende de los argumentos sino de un servidor remoto: la misma llamada devuelve hoy un usuario, mañana un error 500 y pasado un tiempo de espera agotado. Y rompe la ausencia de efectos porque el acto de pedir es en sí mismo una alteración del mundo, observable y a veces irreversible. La lectura del reloj tropieza con lo mismo por otra vía: una hipotética ahora que devolviera el instante actual daría un valor distinto en cada llamada, luego no es una función de sus entradas. El azar es idéntico. Ninguno de estos tres cabe dentro de una función pura sin destruir su pureza.

flowchart LR
Msg[Msg] --> U[update pura]
Model[Model] --> U
U --> Nuevo[Model nuevo]
Mundo[Red reloj azar] -.fuera de su alcance.-> U
style U fill:#a6e3a1,color:#11111b
style Mundo fill:#f38ba8,color:#11111b

El sistema de tipos de Elm hace de este límite algo material, no una recomendación. No existe en la biblioteca estándar ninguna función con un tipo como String -> Respuesta que ejecute una descarga síncrona, ni ninguna () -> Posix que lea el reloj y devuelva un instante. Elm no ofrece esas firmas porque serían mentiras: prometerían pureza donde hay dependencia del mundo. Además, el lenguaje carece de espera bloqueante, así que ni siquiera podrías detener update a la mitad para aguardar la red. El efecto no es que sea difícil de meter en update: es que el lenguaje, deliberadamente, no te da la herramienta para hacerlo.

El dilema y la salida falsa

Ante el muro, la tentación ofrece dos atajos, y los dos son trampas. El primero es relajar la disciplina: permitir que update sea impura y llame a la red directamente. El precio es la ruina completa del edificio, porque la entrada y salida es precisamente el lugar por donde entran el indeterminismo y las excepciones que Elm promete no tener; una update impura ya no es reproducible, ni testeable sin simulacros, ni rebobinable, y la garantía de cero errores en ejecución se evapora. El segundo atajo es el canal lateral: ejecutar el efecto en otro sitio y mutar una variable global que la vista lee. Eso resucita exactamente el caos de estado compartido y mutable que la Arquitectura Elm existe para abolir, disolviendo la fuente única de verdad y el flujo ordenado e inspeccionable de mensajes.

⚠️
La salida falsa: volver impura a update

Hacer que update ejecute el efecto es el error que parece pragmatismo y es demolición. Seduce porque es lo que uno haría en JavaScript imperativo: llamar a fetch donde hace falta el dato. Pero en Elm ese gesto no ahorra trabajo, lo destruye: cada garantía —determinismo, pruebas sin red, viaje en el tiempo, cero excepciones— cuelga de que update sea pura, y basta una sola llamada impura para cortar ese hilo del que cuelgan todas. La pureza no es negociable por partes.

El error de fondo está en la pregunta. Preguntamos cómo dejar que update ejecute efectos, y esa pregunta no tiene respuesta buena. La pregunta fértil es otra: ¿necesita update de verdad ejecutar el efecto, o le basta con decidir que debe ocurrir? Decidir es puro: elegir, a partir de un mensaje y un modelo, que hay que pedir tal recurso a tal URL es una operación determinista y sin efectos. Ejecutar es impuro: abrir el socket, esperar, recibir. Si logramos separar la decisión —un valor que describe el efecto— de la ejecución —que hará alguien más, más tarde—, entonces update sigue siendo pura y la aplicación sigue hablando con el mundo. Ese alguien es el runtime; ese valor es un Cmd. Es el tema de la lección siguiente.

La pureza no es una jaula que rodear, es una frontera que reubicar

La lección profunda de este problema es que no hay que elegir entre pureza y utilidad; hay que dejar de creer que están en conflicto. Un programa que sirve para algo tiene que tocar el mundo, sí, pero tocar el mundo y razonar sobre lo que se hace son dos actividades distintas que la programación imperativa confunde en un mismo punto: la línea donde se ejecuta el efecto es también la línea donde se decide hacerlo. Elm rompe esa fusión. No suprime el efecto —sería absurdo, el efecto es el propósito— sino que traslada la frontera: separa el quién decide del quién ejecuta, y coloca toda la decisión en el lado puro y toda la ejecución en un lado impuro pequeño, único y ajeno a tu lógica. Lo que se logra con eso no es esconder la suciedad, es acorralarla. Cuando el efecto deja de ser algo que tu función hace y pasa a ser algo que tu función describe y devuelve, la parte del programa donde vive el razonamiento queda limpia por construcción, y la parte sucia se reduce a un intérprete que puedes escribir una vez y confiar para siempre. Toda la elegancia de la Arquitectura Elm y, como veremos, de media docena de arquitecturas que la copiaron, cabe en esa reubicación de la frontera: el efecto no se elimina ni se oculta, se convierte en un valor que cruza la línea.

⚔️ Siente el muro antes de ver la puerta
  1. Escribe una update pura para un contador con Incrementar, Decrementar y Reiniciar, y comprueba que puedes probarla aplicando una lista de mensajes sin arrancar nada.
  2. Intenta añadir dentro de update una lectura de la hora del sistema y verifica que el compilador no te ofrece ninguna función pura para hacerlo; explica por qué esa ausencia es intencional.
  3. Enumera tres efectos que tu última aplicación necesitó —una petición, un temporizador, una escritura persistente— y clasifica cada uno como decisión o como ejecución.
  4. Argumenta con transparencia referencial por qué una función que lea el reloj no puede tener el tipo de una función pura, sustituyéndola por su resultado en dos puntos del programa.
  5. Reescribe el dilema en una sola frase que separe con nitidez el acto de decidir que un efecto ocurra del acto de ejecutarlo.
  6. Predice, sin haber leído la lección siguiente, qué debería devolver update además del modelo para describir un efecto sin ejecutarlo, y anota tu conjetura.