Por qué se necesitan los efectos algebraicos
(antelang.org)- 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
mappueden 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 Printocan 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
resumey 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
canque ese efecto puede usarse- Llamar una función de efecto como
say_message: Unit -> Unittoma la forma de “lanzar” un efecto - La función que la invoca expone esa posibilidad en su firma, como
foo () can SayMessage
- Llamar una función de efecto como
- La expresión
handleatrapa el efecto de forma parecida atry/catchy continúa el cálculo interrumpido llamando aresume- Si el handler de
say_messageejecutaprint "Hello World!"y luego llama aresume (), el cálculo original continúa y devuelve42
- Si el handler de
- 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
- generadores
- excepciones
- async
- corrutinas
- diferenciación automática
- 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 eexpresa que, sin importar qué efectoeejecute la función de entradaf,mapejecuta ese mismo efecto- El mismo
mappuede usarse con salida a stdout, llamadas a funciones asíncronas oyieldde streams - Muchos lenguajes con effect handlers permiten omitir la variable de efecto
e, de modo que puede escribirse en la forma familiarmap (input: Vec a) (f: a -> b): Vec b
- Las excepciones pueden implementarse sin llamar a
resumeal manejar el efecto- Se define el efecto
Throw aconthrow: 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
- Se define el efecto
- Los generadores pueden implementarse con el efecto
Yield ayyield: a -> Unit- Se recorren los elementos de un vector llamando a
yield elem - El handler
filtervuelve a llamar ayield xsi el valor emitido cumple la condición y continúa con el siguiente elemento usandoresume () - El handler
my_for_eachejecuta la funciónfpara cada valor emitido y sigue adelante conresume ()
- Se recorren los elementos de un vector llamando a
- 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- El ejemplo de scheduler de Effekt muestra este patró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 aquery "..."
- La forma tradicional recibe el objeto DB como argumento, por ejemplo
- 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_databasepuede ignorar el mensajequeryy hacerresumedevolviendo siempreDbResponse.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_stringatrapa las llamadas aprint msgy las acumula con saltos de línea en la cadenaall_messages output_messagespuede verificar el valor de retorno1234y la cadena de mensajes sin hacer salida real
- El handler
- El logging puede convertirse en salida condicional usando el efecto
LogyLogLevellog_handlerllama aprint msgsi el nivel del mensaje es mayor o igual al umbral configuradofoo () with log_handler Errorimprime 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 apuede verse como un efecto de estado y ofreceget: Unit -> ayset: a -> Unit- El handler
stateconserva el estado inicial, devuelve el contexto actual antegety lo actualiza con el nuevo contexto anteset - La definición de ejemplo de
stateignora las reglas de ownership; en una implementación real podría requerirse la restricciónCopy a
- El handler
- 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_separatoryexampletienen que seguir recibiendostringscomo argumento - En una implementación basada en efectos, las operaciones primitivas
push_stringyget_stringllaman aget/set, y el código de nivel superior ya no necesita pasarstringsdirectamente
- Sin efectos,
- 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
Prngpor todo el programa- Un
Prngglobal es cómodo, pero trae las desventajas de los valores globales, como la necesidad de seguridad de hilos - Con
random: Unit -> U8del efectoRandom, 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/urandomu otra fuente de aleatoriedad, basta con reemplazar el handler y no hace falta modificar el resto del stack
- Un
- La asignación de memoria también puede expresarse como el efecto
Allocateallocate: (size: Usz) -> Alignment -> Ptr afree: 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 conand_thenymap - Azúcar sintáctica como
?de Rust existe para concentrarse en la ruta buena - Con efectos,
get_line_from_stdin (): String can Fail, IOyparse (s: String): U32 can Failpueden escribirse como código secuencial normal:line = ...,x = ...,x * 2
- Con
- El manejo de fallos puede hacerse aplicando un handler para salir de la ruta buena
get_line_from_stdin () with default "42"maneja el efectoFailcon 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.ErrorLibraryB.bar (): U32 can Throw LibraryB.Errormy_functionpuede declarar al mismo tiempoThrow LibraryA.Error,Throw LibraryB.ErroryThrow MyError- Si la repetición se vuelve larga, pueden crearse alias de tipo como
AllErrors = can Throw ... - El mismo efecto
Throw Stringse fusiona en uno solo, y si se quiere separarlo hace falta un tipo wrapper comoMyError
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 Printocan IO, no pueden usarse efectos secundarios - Las definiciones
externno pueden ser verificadas por el compilador, así que hay que confiar en sus definiciones de tipo - Está planeada una función para permitir efectos
IOsolo en modo debug y mantener la seguridad de efectos en builds release
- En Ante, si no se indica algo como
- 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 IOrecibe 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,
recordyreplay, manejan el efecto de nivel superior que exponemain, normalmenteIO recordregistra cuándo ocurre un efecto y su resultado, y luego lo vuelve a elevar al handlerIOintegrado para su manejo realreplayno realizaIOreal y usa los resultados del log de efectos- Si se registra por defecto en builds de debug, puede obtenerse depuración determinista
- Dos handlers,
- 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 -> F64permite saber que no haceIOa 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 requieraIO - Conviene declarar solo los efectos mínimos; por ejemplo,
Printen vez deIOcompleto - 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íaIO - Lo mismo ocurre con el efecto
Fail: una función de biblioteca que antes no fallaba puede empezar a hacerFailmás adelante y propagarse al handlerFailexistente - 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
- Si una función pasa a requerir
- 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
resumeal 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
resumeen absoluto
- Un efecto tail-resumptive es aquel en el que el handler llama a
- 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
resumea 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.