3 puntos por GN⁺ 2025-05-25 | 1 comentarios | Compartir por WhatsApp
  • Los efectos algebraicos (effect handlers) son una herramienta flexible de control de flujo que permite implementar a nivel de biblioteca diversas funciones del lenguaje (manejo de excepciones, generadores, corrutinas, etc.)
  • También pueden aplicarse a gestión de contexto, inyección de dependencias, sustitución del estado global, etc., algo común en la programación funcional
  • Contribuyen a la simplicidad en el diseño de APIs y a automatizar el paso de estado/entorno dentro del código
  • También ofrecen ventajas como garantizar la pureza funcional, replayabilidad y auditoría de seguridad
  • Gracias a avances recientes en tecnología de compiladores, los problemas de rendimiento también han mejorado mucho

Panorama general de los efectos algebraicos (Algebraic Effects)

Los efectos algebraicos (también llamados effect handlers) son una función de los lenguajes de programación que ha ganado mucha atención recientemente. Son una de las funciones centrales de Ante y de varios lenguajes de investigación (Koka, Effekt, Eff, Flix, etc.), y su adopción está creciendo rápidamente. Aunque hay muchos materiales que explican el concepto de los effect handlers, todavía faltan explicaciones profundas sobre por qué son realmente necesarios. Este texto presenta de la manera más amplia posible los usos prácticos y las ventajas de los efectos algebraicos.

Comprensión rápida de la sintaxis y la semántica

  • Los efectos algebraicos son un concepto similar a las “excepciones reanudables”
  • Se pueden declarar funciones de efecto como effect SayMessage
  • Como en foo () can SayMessage = ..., se puede indicar que una función puede usar ese efecto
  • Con handle foo () | say_message () -> ... se pueden manejar de forma similar a un try/catch de excepciones

Esta estructura básica permite invocar y controlar efectos.

Extensión del flujo de control definido por el usuario

La razón más importante para usar efectos algebraicos es que, con una sola función del lenguaje, se pueden implementar como bibliotecas capacidades que antes requerían funciones separadas del lenguaje (generadores, excepciones, corrutinas, asincronía, etc.).

  • Si una función tiene una variable de efecto polimórfica (can e), es posible pasar y combinar distintos efectos como argumentos de la función
  • Por ejemplo, una función map puede declararse para que la función que recibe como argumento pueda usar un efecto arbitrario e, lo que permite combinarla de forma natural con distintos efectos (salida, asincronía, etc.)

Ejemplos de implementación de excepciones y generadores

  • Implementación de excepciones: si, tras disparar un efecto, se maneja sin llamar a resume, se comporta igual que una excepción
  • Implementación de generadores: al definir un efecto Yield, cada vez que se hace yield de un valor, un handler externo puede intervenir y controlar el flujo según condiciones; incluso patrones avanzados como filtrado pueden escribirse con código relativamente simple

La posibilidad de combinar múltiples efectos también es una gran ventaja frente a técnicas anteriores de abstracción de efectos.

Uso como capa de abstracción

Además de ampliar funciones básicas de programación, los efectos algebraicos tienen gran utilidad en diversos escenarios de aplicaciones de negocio.

Inyección de dependencias (Dependency Injection)

  • Dependencias como bases de datos o salida pueden abstraerse como efectos y gestionarse con handlers
  • También es posible implementar con flexibilidad reemplazos con objetos mock para pruebas, redirección de salida, etc.

Logging condicional o control de salida

  • Se puede controlar centralmente si se imprimen mensajes de log según el nivel de logging

Simplificación del diseño de APIs y automatización del paso de contexto

Uso de efectos de estado (State)

  • En situaciones donde hay que pasar un objeto de contexto o información de entorno, si se implementa con base en efectos usando solo get/set, se puede automatizar la gestión del estado sin pasarlo explícitamente
  • Antes había que pasar el contexto como argumento a todas las funciones, pero un state effect permite ocultar esa parte

Sustitución de objetos globales

  • Estados que antes se gestionaban como objetos globales, como generadores de números aleatorios o asignación de memoria, también pueden abstraerse como effects, lo que favorece la claridad del código, la facilidad de pruebas y el soporte de concurrencia
  • Con solo cambiar el handler, se puede modificar de forma flexible la fuente real de aleatoriedad

Soporte para escribir en estilo directo (Direct Style)

  • Antes era necesario manejar varios objetos anidados mediante tipos opción, wrapping de errores, etc.
  • Los efectos permiten expresar de forma limpia rutas de error o efectos secundarios sin necesidad de ese wrapping

Garantía de pureza y auditoría de seguridad

Explicitación de efectos secundarios

  • En la mayoría de los lenguajes con effect handlers, las funciones que producen efectos secundarios deben declarar obligatoriamente efectos como can IO, can Print, etc., en la firma de tipos
  • En casos como creación de hilos o memoria transaccional por software (STM), es indispensable usar funciones puras

Replay de logs y redes deterministas

  • Basándose en la pureza, se pueden crear handlers como record y replay para reproducir resultados de ejecución
  • Esto permite resultados deterministas y soporte de rollback en depuración, bases de datos, redes de videojuegos, etc.

Soporte para seguridad basada en capacidades (Capability-based Security)

  • Como todos los efectos no manejados quedan expuestos en la firma de tipos de una función, esto resulta útil al auditar la seguridad de bibliotecas externas
  • Si una función que antes no tenía efectos secundarios se actualiza y pasa a llevar can IO, el código que la llama puede detectarlo de inmediato

Sin embargo, como todos los efectos se propagan automáticamente, también puede ocurrir el efecto secundario de que se manejen sin darse cuenta.

Perspectiva de eficiencia y conclusión

  • Antes, la eficiencia de ejecución era un punto débil, pero recientemente la optimización ha avanzado mucho en muchos casos, como en efectos tail-resumptive
  • Distintos lenguajes aplican estrategias de compilación eficaces según el caso (closure call, evidence passing, especialización de handlers, etc.)

Se espera que los efectos algebraicos ocupen un lugar mucho más central en los lenguajes de programación del futuro.

1 comentarios

 
GN⁺ 2025-05-25
Comentarios de Hacker News
  • Creo que hay dos desventajas.
    La primera es que, al ver el fragmento de código dado, no hay ninguna indicación de que foo o bar puedan fallar.
    Para saber que este tipo de llamada puede disparar un manejador de errores, hay que ir a buscar la firma de tipos, y según el caso puede tocar hacerlo manualmente sin ayuda del IDE.
    La segunda es que, una vez que entiendes que foo y bar pueden fallar, para encontrar qué código se ejecuta cuando fallan de verdad tienes que subir bastante por la pila de llamadas hasta encontrar una expresión with, y después bajar siguiendo ese manejador.
    No es posible seguir este comportamiento de forma estática ni saltar directo a la definición desde el IDE, porque my_function puede llamarse desde muchos lugares con distintos manejadores.
    Me parece un concepto muy fresco, pero aun así me preocupa en términos de legibilidad del código y depuración.

    • Sobre el problema de encontrar qué código corre cuando falla la ejecución, explican que eso es justamente la esencia de la inyección dinámica de código.
      Igual que otras funciones dinámicas como shallow-binding o deep-binding, el enlace se resuelve siguiendo la pila de llamadas.
      Que no se pueda hacer análisis estático ni usar el salto del IDE también se debe a esa naturaleza dinámica.
      Aun así, creen que en la práctica no hace falta preocuparse demasiado por eso.
      Porque se trata de agregar efectos a código puro, así que según el contexto puede conectarse a mocks de prueba, entornos de producción o efectos puros o impuros.
      Es un principio parecido a la inyección de dependencias.
      En mónadas tradicionales también puede implementarse algo similar, pero para encontrar dónde se instancia realmente la mónada igual hay que mirar la pila de llamadas.
      Estas técnicas tienen ventajas claras, pero también un costo evidente.
      Son útiles para testing y sandboxing, pero hacen menos visible qué está pasando realmente en el código.

    • Comparten que escribieron una tesis de licenciatura sobre soporte de IDE para efectos y manejadores léxicos.
      Piensan que todos los puntos señalados arriba son perfectamente viables.
      Enlace al artículo

    • Mencionan que en el mundo .NET hay una tendencia a abusar de las interfaces, lo que obliga a pasar por varios pasos para saltar directo a la implementación de un método.
      A menudo, si la implementación está en otro assembly, las funciones del IDE dejan de servir.
      En Dependency Injection avanzada, especialmente con Autofac, se construyen scopes jerárquicos como las variables de scope dinámico de LISP para decidir en tiempo de ejecución a qué instancia se enlaza un servicio.
      En ese sentido, se podría inyectar un efecto como una instancia de interfaz tipo ISomeEffectHandler y representarlo como una llamada a ese método cuando se dispara el efecto.
      El comportamiento concreto del manejador, como lanzar excepciones o registrar logs, se decidiría dinámicamente según la configuración de DI.
      Antes usaban el patrón de hacer throw de excepciones, pero podría migrarse a un diseño que explicite los efectos mediante interfaces y delegue por completo el tratamiento a DI.
      No alcanzaron a profundizar en temas de iteradores como yield.

    • Creen que el punto clave es justamente que no haya indicación de que foo y bar puedan fallar.
      Eso permite escribir código en estilo directo sin preocuparse por el contexto efectivo.
      Encontrar qué código corre al fallar también es la esencia de la abstracción.
      Qué manejador de efectos se acoplará realmente en ejecución se decide después.
      Es el mismo principio que en f : g:(A -> B) -> t(A) -> B, donde no puedes saber por adelantado qué código correrá cuando se ejecute g.

    • No están de acuerdo con la afirmación de que sea imposible hacer análisis estático subiendo por la pila de llamadas para encontrar el manejador.
      En la práctica sí puede hacerse análisis estático, y desde el IDE podrías usar algo como “ir al caller” para elegir qué manejador se está usando.

  • El "pseudocódigo" de Ante les pareció muy impresionante.
    Da la sensación de una mezcla muy lograda entre las características de Haskell y la expresividad y practicidad de Elixir.
    Les da la impresión de ser un Haskell para desarrolladores.
    Esperan que el compilador madure.
    Les gustaría probar desarrollar una app con Ante.

  • Sobre la afirmación de que AE (Algebraic Effects) generaliza el flujo de control y también puede implementar corrutinas:
    creen que la forma más simple de implementar AE en un runtime de lenguaje nuevo es justamente usar corrutinas y poner encima, a nivel sintáctico, la estructura básica de yield/resume.
    Preguntan si se les está escapando algo.

    • Como diferencia principal entre AE y las corrutinas, señalan la seguridad de tipos.
      En AE puedes declarar qué efectos puede usar una función directamente en el código fuente.
      Por ejemplo, si algo como query_db(): User can Database, eso significa que puede acceder a la base de datos y que al llamarla necesariamente debes proporcionar un manejador de Database.
      La estructura deja muy claro qué se puede y qué no se puede hacer.
      Igual que en NextJS los server components no pueden usar directamente funcionalidades del cliente, este tipo de restricciones de seguridad resulta popular en muchos ámbitos.

    • Effect-TS se acerca a este enfoque en JavaScript usando corrutinas, pero no están seguros de que al final sea una buena idea.
      Les preocupa que, igual que la DI del framework Spring, AE termine expandiéndose por todo el código y solo agregue complejidad.
      De hecho, critican que la mayoría de las charlas en EffectDays sobre uso de efectos en frontend eran puro boilerplate sin sentido.
      AE es un concepto fascinante, pero creen que la carga de envolver muchas operaciones en funciones puede perjudicar la facilidad característica de escribir código en JS.
      En cambio, también ven una gran ventaja en enfoques como motioncanvas, donde solo con corrutinas se pueden expresar fácilmente escenarios complejos de gráficos 2D.
      Video relacionado de EffectDays
      MotionCanvas

    • Se dice que dentro de un hilo los manejadores de AE pueden reanudar (resume) el código varias veces, como call/cc.
      En cambio, en las corrutinas solo puede reanudarse una vez por cada yield.
      Como ese flujo de ejecución incierto hace más difícil la predicción, prefieren devolver explícitamente funciones que puedan llamarse varias veces o reemplazarlo por otras estructuras como iteradores.

  • Hay quienes sienten que este concepto resulta extremadamente atractivo como abstracción de programación.
    Programando kernels en Sun, les parecía una gran ventaja poder escribir código de forma concisa al hacer una llamada como sleep(foo) y continuar luego cuando foo lo despertaba.
    Disminuye la carga de manejar uno por uno los casos límite mediante flujo de control.
    Si se cuidan los temas de localidad de memoria, parece divertido inicializar varias funciones por adelantado en estado de espera y expresar el algoritmo directamente como mutaciones de cada unidad.

  • Sobre la afirmación de que “los efectos algebraicos son como excepciones que se pueden reanudar”:
    preguntan en qué se diferencian realmente de type classes como ApplicativeError o MonadError.
    La forma de declarar los efectos que una función puede usar se parece a las checked exceptions, y manejar efectos con una expresión handle es casi igual a try/catch.
    Estas type classes ya soportan capturar excepciones con mecanismos como handleError/handleErrorWith.
    Se dice que los efectos algebraicos tendrán ventajas en los lenguajes del “futuro”, pero en realidad creen que es una idea que ya hoy se usa bastante.
    Enlace explicativo de cats

    • Si solo se trata un único efecto, quizá no haya una gran diferencia, pero cuando necesitas varios efectos a la vez, el soporte directo para efectos es mucho más limpio e intuitivo que anidar mónadas explícitamente.
      Al combinar mónadas, aparecen problemas molestos como fijar el orden o tener que cambiarlo cuando el conjunto de mónadas esperado por el resultado de una función no coincide.

    • Personalmente creen que mónadas y efectos no compiten, sino que se entienden mejor como interpretaciones complementarias.
      Recomiendan ver artículos relacionados, como el paper de Koka.

    • Los efectos algebraicos funcionan sobre la pila del programa, como las delimited continuations.
      Con un simple truco monádico no puedes saltar de inmediato al manejador de efectos que está cinco frames más arriba en la pila, modificar solo variables locales en ese frame y luego volver cinco niveles hacia abajo.

    • La diferencia está en el comportamiento estático vs dinámico.
      Al programar con mónadas tienes que implementar directamente todos los métodos relacionados, pero en un sistema de efectos puedes instalar manejadores de efectos dinámicamente en cualquier momento y sobrescribir con flexibilidad los existentes.
      Por ejemplo, es posible usar por debajo una mónada especializada con propiedades de IO para testing e instalar manejadores de efectos solo por debajo de ella, formando una estructura compuesta.

    • Las similitudes son grandes, pero la usabilidad cambia bastante.
      Los efectos algebraicos se parecen a una mónada free, pero como vienen integrados, la sintaxis es más simple y también componen mejor.
      En lenguajes centrados en mónadas como Haskell, con inferencia de type classes (estilo mtl) y bind integrado como sintaxis, se puede lograr algo que superficialmente se parece.

  • Antes pensaban, equivocadamente, que los efectos algebraicos solo se trataban dentro de sistemas de tipos estáticos, pero hace poco descubrieron que también existen estructuras dinámicas.
    Les impresionaron especialmente dos artículos antiguos sobre la versión dinámica de Eff (primero, segundo).
    Conceptos como “operaciones parametrizadas con aridad generalizada” también les parecen interesantes al conectar abstracción y programación.

    • Preguntan qué es lo que no les gusta de los sistemas de tipos estáticos.
  • Mencionan que se trata de una idea vieja que reaparece recientemente con un nombre y un marco nuevos.
    Introducción al LISP Condition System
    Probando Algebraic Effects

  • Tuvieron experiencia usando effects en la alpha de OCaml 5 para hacer protohackers.
    Fue divertido, aunque en ese momento la toolchain era algo incómoda.
    Como Ante da una sensación parecida, esperan ver cómo evoluciona.

    • Los effects de OCaml desde la versión 5.3 han mejorado muchísimo frente a antes.
      Todavía no tienen un sistema de tipos encima, pero ahora sí se sienten claramente más pulidos.
  • Después de pasar mucho tiempo en Prolog, están buscando un lenguaje que facilite componer funciones no deterministas y hacer chequeo de tipos en tiempo de compilación.
    Ante es uno de los candidatos que les interesa.
    También comentan que no hay que olvidar herramientas para desarrolladores y plugins de editor como LSP o tree-sitter.

    • Como autor de Ante, responden que ya hay soporte para LSP, aunque muy básico.
      Creen que todo lenguaje nuevo necesita tooling desde el principio.
      También les importa la experiencia de depuración, así que están considerando si al menos en modo debug podrían ofrecer por defecto alguna capacidad de replay.
  • Sobre la afirmación de que “los efectos algebraicos son como excepciones que se pueden reanudar”:
    preguntan si se parecen a las conditions de Common Lisp.
    Les parece interesante que ideas viejas reaparezcan simplemente con otro nombre.

    • Los efectos algebraicos son mucho más amplios que el sistema de conditions de LISP.
      Como las continuations pueden ser multi-shot, se parecen al call/cc de Scheme.
      También mencionan que este paralelismo podría llevar a resultados peores que no tenerlo.

    • En Smalltalk existen las “resumable exceptions”.

    • Creen que si se considera a los efectos solo como un cambio de nombre del viejo condition system, la discusión no avanza mucho.
      Los efectos algebraicos que se discuten hoy tienen diferencias que van más allá de un concepto simple.

    • También puede mencionarse Dependency Injection en un contexto parecido.