Por qué se necesitan los efectos algebraicos
(antelang.org)- 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
mappuede declararse para que la función que recibe como argumento pueda usar un efecto arbitrarioe, 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
recordyreplaypara 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
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
fooobarpuedan 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
fooybarpueden 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ónwith, 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_functionpuede 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
ISomeEffectHandlery 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
throwde 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
fooybarpuedan 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 ejecuteg.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 deDatabase.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, comocall/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 cuandofoolo 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
ApplicativeErroroMonadError.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
handlees casi igual atry/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
bindintegrado 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.
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.
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.
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/ccde 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.