3 puntos por GN⁺ 2025-05-25 | Aún no hay comentarios. | Compartir por WhatsApp
  • Los efectos algebraicos son una función del lenguaje que captura y maneja el flujo de control como excepciones reanudables; son una función central de Ante y también se usan de forma central en lenguajes de investigación como Koka, Effekt, Eff y Flix
  • Con el mismo mecanismo se pueden crear a nivel de biblioteca generadores, excepciones, async, corrutinas y diferenciación automática, y gracias al polimorfismo de efectos, funciones como map pueden escribirse una sola vez sin importar el tipo de efecto
  • Si se convierten en efectos la inyección de dependencias y el paso de contexto para acceso a bases de datos, salida, logging o manejo de estado, los mocks de prueba, la recolección de salida y el filtrado de logs pueden resolverse cambiando handlers
  • Si en la firma de una función aparecen efectos como can IO, can Print o can Fail, eso favorece la garantía de pureza, el registro/reproducción y la auditoría de seguridad, aunque los efectos ya permitidos pueden propagarse sin querer al handler existente
  • La debilidad tradicional era la preocupación por la eficiencia, pero los lenguajes recientes están reduciendo el costo con optimizaciones para efectos tail-resumptive, evidence passing, restricción a un solo resume y especialización de handlers

Modelo básico de los efectos algebraicos

  • Los efectos algebraicos también se conocen como effect handlers, y pueden entenderse con el modelo de “excepciones reanudables”
  • En el pseudocódigo de Ante, se declaran funciones de efecto y en la firma de la función se indica con can que ese efecto puede usarse
    • Llamar una función de efecto como say_message: Unit -> Unit toma la forma de “lanzar” un efecto
    • La función que la invoca expone esa posibilidad en su firma, como foo () can SayMessage
  • La expresión handle atrapa el efecto de forma parecida a try/catch y continúa el cálculo interrumpido llamando a resume
    • Si el handler de say_message ejecuta print "Hello World!" y luego llama a resume (), el cálculo original continúa y devuelve 42
  • El nombre “algebraic” es en gran parte un término heredado; en la práctica, effect handlers suele ser una descripción más precisa, pero se usa “efectos algebraicos” por ser el nombre más familiar para los usuarios

Flujo de control definido por el usuario

  • Los efectos algebraicos permiten implementar varias funciones del lenguaje con un solo mecanismo
  • El polimorfismo de efectos reduce el problema de what color is your function
    • map (input: Vec a) (f: a -> b can e): Vec b can e expresa que, sin importar qué efecto e ejecute la función de entrada f, map ejecuta ese mismo efecto
    • El mismo map puede usarse con salida a stdout, llamadas a funciones asíncronas o yield de streams
    • Muchos lenguajes con effect handlers permiten omitir la variable de efecto e, de modo que puede escribirse en la forma familiar map (input: Vec a) (f: a -> b): Vec b
  • Las excepciones pueden implementarse sin llamar a resume al manejar el efecto
    • Se define el efecto Throw a con throw: a -> never_returns
    • Si se divide entre 0, se llama a throw "error: Division by zero!", y el handler imprime el mensaje sin reanudar el cálculo
  • Los generadores pueden implementarse con el efecto Yield a y yield: a -> Unit
    • Se recorren los elementos de un vector llamando a yield elem
    • El handler filter vuelve a llamar a yield x si el valor emitido cumple la condición y continúa con el siguiente elemento usando resume ()
    • El handler my_for_each ejecuta la función f para cada valor emitido y sigue adelante con resume ()
  • También puede construirse un scheduler cooperativo con el efecto yield: Unit -> Unit, donde el handler toma el control y cambia a ejecutar otra función
  • Múltiples efectos se componen bien entre sí, y eso se considera una ventaja de usabilidad frente a otras abstracciones de efectos

Inyección de dependencias y capacidad de prueba

  • Los efectos también pueden usarse para inyección de dependencias en aplicaciones de negocio comunes
  • En vez de pasar directamente un objeto de base de datos como argumento, puede definirse un efecto Database
    • La forma tradicional recibe el objeto DB como argumento, por ejemplo business_logic (db: Database) (x: I32)
    • La forma basada en efectos pasa a ser business_logic (x: I32) can Database, y dentro se llama a query "..."
  • La selección de la base de datos concreta queda a cargo del handler en una parte superior del stack de llamadas
    • Puede cambiarse la DB de producción por otra distinta o reemplazarse por una DB mock para pruebas
    • El handler mock_database puede ignorar el mensaje query y hacer resume devolviendo siempre DbResponse.Ok
  • Si la salida también se trata como efecto, durante las pruebas puede recolectarse como texto sin escribir directamente en stdout
    • El handler print_to_string atrapa las llamadas a print msg y las acumula con saltos de línea en la cadena all_messages
    • output_messages puede verificar el valor de retorno 1234 y la cadena de mensajes sin hacer salida real
  • El logging puede convertirse en salida condicional usando el efecto Log y LogLevel
    • log_handler llama a print msg si el nivel del mensaje es mayor o igual al umbral configurado
    • foo () with log_handler Error imprime solo los logs de error

APIs más limpias y paso de contexto

  • Los efectos algebraicos pueden expresar como efectos el patrón de objeto Context que se pasa por todo un programa o biblioteca
  • El efecto Use a puede verse como un efecto de estado y ofrece get: Unit -> a y set: a -> Unit
    • El handler state conserva el estado inicial, devuelve el contexto actual ante get y lo actualiza con el nuevo contexto ante set
    • La definición de ejemplo de state ignora las reglas de ownership; en una implementación real podría requerirse la restricción Copy a
  • El ejemplo que guarda cadenas dentro de un vector y pasa índices como si fueran claves muestra el costo de pasar contexto
    • Sin efectos, push_string, get_string, append_with_separator y example tienen que seguir recibiendo strings como argumento
    • En una implementación basada en efectos, las operaciones primitivas push_string y get_string llaman a get/set, y el código de nivel superior ya no necesita pasar strings directamente
  • Este enfoque encaja bien cuando una biblioteca encapsula el paso interno de contexto
    • Quien usa la biblioteca no necesita preocuparse por los detalles internos de cómo se pasa el contexto
    • Si se quiere evitar depender de un tipo de contexto específico, las funciones necesarias pueden abstraerse como una interfaz

Reemplazo de variables globales y estilo directo

  • APIs que por fuera parecen sin estado, como la generación de números aleatorios o la asignación de memoria, pero que en realidad necesitan estado, pueden expresarse con efectos en lugar de variables globales
  • El ejemplo de generación aleatoria muestra la carga de tener que pasar un objeto Prng por todo el programa
    • Un Prng global es cómodo, pero trae las desventajas de los valores globales, como la necesidad de seguridad de hilos
    • Con random: Unit -> U8 del efecto Random, el usuario solo necesita indicar que en algún punto superior del stack de llamadas hay una inicialización mediante un handler
    • Después, para cambiar a /dev/urandom u otra fuente de aleatoriedad, basta con reemplazar el handler y no hace falta modificar el resto del stack
  • La asignación de memoria también puede expresarse como el efecto Allocate
    • allocate: (size: Usz) -> Alignment -> Ptr a
    • free: Ptr a -> Unit
    • La mayoría de las llamadas pueden usar un allocator global, y dentro de un tight loop puede añadirse un handler al cuerpo del loop para cambiar a un arena allocator
  • Los efectos permiten estilo directo en lugar de pasar resultados envueltos en valores dedicados
    • Con Maybe t, la ruta exitosa debe encadenarse con and_then y map
    • Azúcar sintáctica como ? de Rust existe para concentrarse en la ruta buena
    • Con efectos, get_line_from_stdin (): String can Fail, IO y parse (s: String): U32 can Fail pueden escribirse como código secuencial normal: line = ..., x = ..., x * 2
  • El manejo de fallos puede hacerse aplicando un handler para salir de la ruta buena
    • get_line_from_stdin () with default "42" maneja el efecto Fail con un valor por defecto
  • Los distintos tipos de error también se componen naturalmente como una lista de efectos
    • LibraryA.foo (): U32 can Throw LibraryA.Error
    • LibraryB.bar (): U32 can Throw LibraryB.Error
    • my_function puede declarar al mismo tiempo Throw LibraryA.Error, Throw LibraryB.Error y Throw MyError
    • Si la repetición se vuelve larga, pueden crearse alias de tipo como AllErrors = can Throw ...
    • El mismo efecto Throw String se fusiona en uno solo, y si se quiere separarlo hace falta un tipo wrapper como MyError

Pureza, reejecución y auditoría de seguridad

  • La mayoría de los lenguajes con effect handlers, salvo casos como OCaml, usan efectos en los lugares donde pueden ocurrir efectos secundarios
    • En Ante, si no se indica algo como can Print o can IO, no pueden usarse efectos secundarios
    • Las definiciones extern no pueden ser verificadas por el compilador, así que hay que confiar en sus definiciones de tipo
    • Está planeada una función para permitir efectos IO solo en modo debug y mantener la seguridad de efectos en builds release
  • Algunas funciones requieren como entrada funciones puras
    • Al crear hilos, el hilo generado no debería poder invocar handlers que pertenezcan al hilo actual
    • spawn_all (functions: Vec (Unit -> a pure)): Vec a can IO recibe solo funciones puras, ejecuta todas en hilos y espera a que terminen
  • Software Transactional Memory (STM) es una técnica de concurrencia que requiere funciones puras
    • Si varias funciones se ejecutan al mismo tiempo y durante una transacción otro hilo cambia un valor, esa transacción se reinicia
    • Hay una implementación de prueba de concepto en Effekt en effekt-stm
  • La pureza también puede habilitar registro/reproducción similar a la utilidad de depuración rr
    • Dos handlers, record y replay, manejan el efecto de nivel superior que expone main, normalmente IO
    • record registra cuándo ocurre un efecto y su resultado, y luego lo vuelve a elevar al handler IO integrado para su manejo real
    • replay no realiza IO real y usa los resultados del log de efectos
    • Si se registra por defecto en builds de debug, puede obtenerse depuración determinista
  • La lista de efectos en la firma de una función ayuda a la auditoría de seguridad, de forma parecida a Capability Based Security
    • get_pi: Unit -> F64 permite saber que no hace IO a escondidas en segundo plano
    • Si después de una actualización de biblioteca cambia a get_pi: Unit -> F64 can IO, el código que la llama producirá un error a menos que esa función que llama ya requiera IO
    • Conviene declarar solo los efectos mínimos; por ejemplo, Print en vez de IO completo
    • Agregar un efecto nuevo se considera un cambio que rompe semantic versioning
    • Material relacionado: Capability Based Security y Designing with Static Capabilities and Effects

Límites y estrategias de implementación

  • Una de las limitaciones del enfoque con efectos es la posibilidad de manejo no intencional
    • Si una función pasa a requerir IO, puede que no aparezca ningún error si la función que la llama ya permitía IO
    • Lo mismo ocurre con el efecto Fail: una función de biblioteca que antes no fallaba puede empezar a hacer Fail más adelante y propagarse al handler Fail existente
    • Dependiendo del caso, este comportamiento puede ser aceptable, pero si se esperaba un tratamiento aparte, como proveer un valor por defecto, puede no coincidir con la intención
  • El principal inconveniente tradicional era la preocupación por la eficiencia, pero la salida compilada de los efectos ha mejorado mucho en tiempos recientes
  • Muchos lenguajes de efectos algebraicos optimizan los efectos tail-resumptive como una llamada normal a un closure
    • Un efecto tail-resumptive es aquel en el que el handler llama a resume al final
    • La mayoría de los efectos reales pertenecen a esta categoría, y la mayoría de los ejemplos del texto también entran ahí
    • Las excepciones se consideran un caso aparte, porque no llaman a resume en absoluto
  • Las estrategias de optimización varían según el lenguaje
    • Koka usa evidence passing y eleva los efectos hasta sus handlers para compilar a C sin runtime
    • Ante y OCaml restringen resume a llamarse como máximo una vez
      • Esta restricción excluye algunos efectos como la no determinación
      • A cambio, simplifica el manejo de recursos y permite implementar continuations internas de forma más eficiente con técnicas como segmented stacks
    • Effekt especializa completamente los handlers y los elimina del programa
      • Este enfoque tiene la limitación de volver second-class a la mayoría de las funciones
      • Es posible obtener funciones first-class en forma boxed y cambiar al modelo pay-as-you-go
      • Material relacionado: documentación de captures de Effekt y paper

Aún no hay comentarios.

Aún no hay comentarios.