En informática , la programación reactiva es un paradigma de programación declarativa que se ocupa de los flujos de datos y la propagación del cambio. Con este paradigma es posible expresar flujos de datos estáticos (por ejemplo, matrices) o dinámicos (por ejemplo, emisores de eventos) con facilidad, y también comunicar que existe una dependencia inferida dentro del modelo de ejecución asociado , lo que facilita la propagación automática de los datos modificados. flujo. [ cita requerida ]
Por ejemplo, en un entorno de programación imperativo , significaría que se le asigna el resultado de en el instante en que se evalúa la expresión, y posteriormente, los valores de y se puede cambiar sin ningún efecto sobre el valor de . Por otro lado, en la programación reactiva , el valor de se actualiza automáticamente siempre que los valores de o cambiar, sin que el programa tenga que volver a ejecutar explícitamente la declaración para determinar el valor actualmente asignado de [ cita requerida ]
var b = 1var c = 2var a = b + cb = 10console.log (a) // 3 (no 12 porque "=" no es un operador de asignación reactivo)// ahora imagina que tienes un operador especial "$ =" que cambia el valor de una variable (ejecuta el código en el lado derecho del operador y asigna el resultado a la variable del lado izquierdo) no solo cuando se inicializa explícitamente, sino también cuando se hace referencia a variables ( en el lado derecho del operador) se cambianvar b = 1var c = 2var a $ = b + cb = 10console.log (a) // 12
Otro ejemplo es un lenguaje de descripción de hardware como Verilog , donde la programación reactiva permite modelar los cambios a medida que se propagan a través de los circuitos. [ cita requerida ]
La programación reactiva se ha propuesto como una forma de simplificar la creación de interfaces de usuario interactivas y la animación del sistema casi en tiempo real. [ cita requerida ]
Por ejemplo, en una arquitectura modelo-vista-controlador (MVC), la programación reactiva puede facilitar cambios en un modelo subyacente que se reflejan automáticamente en una vista asociada . [1]
Enfoques para la creación de lenguajes de programación reactivos
Se emplean varios enfoques populares en la creación de lenguajes de programación reactivos. Especificación de lenguajes dedicados que son específicos de varias restricciones de dominio . Estas limitaciones suelen caracterizarse por una descripción de hardware o de computación incorporada en tiempo real. Otro enfoque implica la especificación de lenguajes de propósito general que incluyen soporte para reactividad. Otros enfoques se articulan en la definición y el uso de bibliotecas de programación , o lenguajes específicos de dominio integrados , que permiten la reactividad junto o encima del lenguaje de programación. La especificación y el uso de estos diferentes enfoques dan lugar a compensaciones de capacidad lingüística . En general, cuanto más restringido es un lenguaje, más sus compiladores y herramientas de análisis asociados pueden informar a los desarrolladores (por ejemplo, al realizar análisis para determinar si los programas pueden ejecutarse en tiempo real). Las compensaciones funcionales en la especificidad pueden resultar en el deterioro de la aplicabilidad general de un idioma.
Modelos de programación y semántica
Una variedad de modelos y semánticas gobiernan la familia de programación reactiva. Podemos dividirlos libremente en las siguientes dimensiones:
- Sincronía: ¿es el modelo subyacente de tiempo sincrónico versus asincrónico?
- Determinismo: determinista versus no determinista tanto en el proceso de evaluación como en los resultados
- Proceso de actualización: devoluciones de llamada versus flujo de datos versus actor
Técnicas y desafíos de implementación
Esencia de implementaciones
Los tiempos de ejecución del lenguaje de programación reactivo están representados por un gráfico que identifica las dependencias entre los valores reactivos involucrados. En un gráfico de este tipo, los nodos representan el acto de computar y las relaciones de dependencia del modelo de bordes . Tal tiempo de ejecución emplea dicho gráfico, para ayudarlo a realizar un seguimiento de los diversos cálculos, que deben ejecutarse de nuevo, una vez que una entrada involucrada cambia de valor.
Cambiar algoritmos de propagación
Los enfoques más comunes para la propagación de datos son:
- Pull : El consumidor de valor es de hecho proactivo , ya que consulta regularmente la fuente observada en busca de valores y reacciona siempre que hay un valor relevante disponible. Esta práctica de verificar regularmente eventos o cambios de valor se conoce comúnmente como sondeo .
- Empujar : el consumidor de valor recibe un valor de la fuente siempre que el valor está disponible. Estos valores son independientes, por ejemplo, contienen toda la información necesaria y el consumidor no necesita consultar más información.
- Push-pull : El consumidor de valor recibe una notificación de cambio , que es una breve descripción del cambio, por ejemplo, "algún valor cambiado" - esta es la parte de push . Sin embargo, la notificación no contiene toda la información necesaria (es decir, no contiene los valores reales), por lo que el consumidor debe consultar la fuente para obtener más información (el valor específico) después de recibir la notificación; esta es la parte de extracción . Este método se usa comúnmente cuando hay un gran volumen de datos en los que los consumidores podrían estar potencialmente interesados. Por lo tanto, para reducir el rendimiento y la latencia, solo se envían notificaciones ligeras; y luego aquellos consumidores que requieran más información solicitarán esa información específica. Este enfoque también tiene el inconveniente de que la fuente puede verse abrumada por muchas solicitudes de información adicional después de que se envía una notificación.
¿Qué empujar?
En el nivel de implementación, la reacción al evento consiste en la propagación a través de la información de un gráfico, que caracteriza la existencia del cambio. En consecuencia, los cálculos que se ven afectados por dicho cambio quedan obsoletos y deben marcarse para volver a ejecutarlos. Dichos cálculos se caracterizan generalmente por el cierre transitivo del cambio en su fuente asociada. La propagación del cambio puede conducir a una actualización del valor de los sumideros del gráfico .
La información propagada por el gráfico puede consistir en el estado completo de un nodo, es decir, el resultado del cálculo del nodo involucrado. En tales casos, la salida anterior del nodo se ignora. Otro método implica la propagación delta, es decir, la propagación de cambios incrementales . En este caso, la información se propaga a lo largo de los bordes de un gráfico , que consisten solo en delta que describen cómo se cambió el nodo anterior. Este enfoque es especialmente importante cuando los nodos contienen grandes cantidades de datos de estado , que de otro modo sería costoso volver a calcular desde cero.
La propagación delta es esencialmente una optimización que ha sido ampliamente estudiada a través de la disciplina de la computación incremental , cuyo enfoque requiere satisfacción en tiempo de ejecución que involucra el problema de actualización de vista . Este problema se caracteriza infamemente por el uso de entidades de base de datos , que son responsables del mantenimiento de las vistas de datos cambiantes.
Otra optimización común es el empleo de la acumulación de cambios unarios y la propagación por lotes . Esta solución puede ser más rápida porque reduce la comunicación entre los nodos involucrados. A continuación, se pueden emplear estrategias de optimización que expliquen la naturaleza de los cambios contenidos en su interior y realizar modificaciones en consecuencia. por ejemplo, dos cambios en el lote pueden cancelarse entre sí y, por lo tanto, simplemente pueden ignorarse. Otro enfoque disponible más se describe como propagación de notificación de invalidez . Este enfoque hace que los nodos con entradas no válidas extraigan actualizaciones, lo que da como resultado la actualización de sus propias salidas.
Hay dos formas principales empleadas en la construcción de un gráfico de dependencia :
- El gráfico de dependencias se mantiene implícitamente dentro de un bucle de eventos . El registro de devoluciones de llamada explícitas da como resultado la creación de dependencias implícitas. Por lo tanto, la inversión de control , que se induce a través de la devolución de llamada, se deja en su lugar. Sin embargo, hacer que las devoluciones de llamada sean funcionales (es decir, devolver un valor de estado en lugar de un valor unitario) requiere que dichas devoluciones de llamada se vuelvan composicionales.
- Un gráfico de dependencias es específico del programa y lo genera un programador. Esto facilita el direccionamiento de la inversión de control de la devolución de llamada de dos maneras: o bien un gráfico se especifica explícitamente (generalmente usando un lenguaje específico de dominio (DSL), que puede estar incrustado), o un gráfico se define implícitamente con expresión y generación usando un lenguaje efectivo. , lenguaje arquetípico .
Desafíos de implementación en la programación reactiva
Fallas
Al propagar cambios, es posible elegir órdenes de propagación de modo que el valor de una expresión no sea una consecuencia natural del programa fuente. Podemos ilustrar esto fácilmente con un ejemplo. Supongamos que seconds
es un valor reactivo que cambia cada segundo para representar la hora actual (en segundos). Considere esta expresión:
t = segundos + 1g = (t> segundos)
Debido a t
que siempre debe ser mayor que seconds
, esta expresión siempre debe evaluarse como un valor verdadero. Desafortunadamente, esto puede depender del orden de evaluación. Cuando seconds
cambia, dos expresiones deben actualizarse: seconds + 1
y la condicional. Si el primero se evalúa antes que el segundo, entonces se mantendrá este invariante. Sin embargo, si el condicional se actualiza primero, utilizando el valor antiguo de t
y el nuevo valor de seconds
, entonces la expresión se evaluará como un valor falso. A esto se le llama falla .
Algunos lenguajes reactivos están libres de fallas y prueban esta propiedad [ cita requerida ] . Por lo general, esto se logra ordenando topológicamente las expresiones y actualizando los valores en orden topológico. Sin embargo, esto puede tener implicaciones en el rendimiento, como retrasar la entrega de valores (debido al orden de propagación). En algunos casos, por lo tanto, los lenguajes reactivos permiten fallas y los desarrolladores deben ser conscientes de la posibilidad de que los valores no se correspondan temporalmente con la fuente del programa, y que algunas expresiones pueden evaluarse varias veces (por ejemplo, t > seconds
pueden evaluarse dos veces: una vez cuando el nuevo valor de seconds
llega, y una vez más cuando se t
actualiza).
Dependencias cíclicas
La clasificación topológica de las dependencias depende de que el gráfico de dependencia sea un gráfico acíclico dirigido (DAG). En la práctica, un programa puede definir un gráfico de dependencia que tiene ciclos. Por lo general, los lenguajes de programación reactiva esperan que tales ciclos se "rompan" colocando algún elemento a lo largo de un "borde posterior" para permitir que la actualización reactiva termine. Normalmente, los lenguajes proporcionan un operador como el delay
que utiliza el mecanismo de actualización para este propósito, ya que a delay
implica que lo que sigue debe evaluarse en el "siguiente paso de tiempo" (permitiendo que la evaluación actual termine).
Interacción con el estado mutable
Los lenguajes reactivos suelen asumir que sus expresiones son puramente funcionales . Esto permite que un mecanismo de actualización elija diferentes órdenes en los que realizar las actualizaciones y deje el orden específico sin especificar (permitiendo así las optimizaciones). Sin embargo, cuando un lenguaje reactivo está incrustado en un lenguaje de programación con estado, es posible que los programadores realicen operaciones mutables. Cómo hacer que esta interacción sea fluida sigue siendo un problema abierto.
En algunos casos, es posible tener soluciones parciales basadas en principios. Dos de estas soluciones incluyen:
- Un lenguaje puede ofrecer una noción de "célula mutable". Una celda mutable es aquella de la que es consciente el sistema de actualización reactiva, de modo que los cambios realizados en la celda se propagan al resto del programa reactivo. Esto permite que la parte no reactiva del programa realice una mutación tradicional al tiempo que permite que el código reactivo conozca y responda a esta actualización, manteniendo así la coherencia de la relación entre los valores en el programa. Un ejemplo de lenguaje reactivo que proporciona una celda de este tipo es FrTime. [2]
- Las bibliotecas orientadas a objetos correctamente encapsuladas ofrecen una noción de estado encapsulada. En principio, por lo tanto, es posible que una biblioteca de este tipo interactúe sin problemas con la parte reactiva de un idioma. Por ejemplo, se pueden instalar devoluciones de llamada en los captadores de la biblioteca orientada a objetos para notificar al motor de actualización reactiva sobre los cambios de estado, y los cambios en el componente reactivo se pueden enviar a la biblioteca orientada a objetos a través de captadores. FrTime emplea esa estrategia. [3]
Actualización dinámica del gráfico de dependencias
En algunos lenguajes reactivos, el gráfico de dependencias es estático , es decir, el gráfico es fijo durante la ejecución del programa. En otros lenguajes, el gráfico puede ser dinámico , es decir, puede cambiar a medida que se ejecuta el programa. Para un ejemplo simple, considere este ejemplo ilustrativo (donde seconds
es un valor reactivo):
t = si ((segundos mod 2) == 0): segundos + 1 demás: segundos - 1 finalt + 1
Cada segundo, el valor de esta expresión cambia a una expresión reactiva diferente, que t + 1
luego depende de. Por lo tanto, el gráfico de dependencias se actualiza cada segundo.
Permitir la actualización dinámica de las dependencias proporciona un poder expresivo significativo (por ejemplo, las dependencias dinámicas ocurren de forma rutinaria en los programas de interfaz gráfica de usuario (GUI)). Sin embargo, el motor de actualización reactiva debe decidir si reconstruir las expresiones cada vez o mantener el nodo de una expresión construido pero inactivo; en el último caso, asegúrese de que no participen en el cálculo cuando no se supone que estén activos.
Conceptos
Grados de explicidad
Los lenguajes de programación reactiva pueden variar desde los muy explícitos en los que los flujos de datos se configuran mediante el uso de flechas, hasta los implícitos en los que los flujos de datos se derivan de construcciones de lenguaje que se parecen a las de la programación imperativa o funcional. Por ejemplo, en la programación reactiva funcional (FRP) implícitamente levantada, una llamada de función puede causar implícitamente que se construya un nodo en un gráfico de flujo de datos. Las bibliotecas de programación reactiva para lenguajes dinámicos (como las bibliotecas Lisp "Cells" y Python "Trellis") pueden construir un gráfico de dependencia a partir del análisis en tiempo de ejecución de los valores leídos durante la ejecución de una función, permitiendo que las especificaciones de flujo de datos sean tanto implícitas como dinámicas.
A veces, el término programación reactiva se refiere al nivel arquitectónico de la ingeniería de software, donde los nodos individuales en el gráfico de flujo de datos son programas ordinarios que se comunican entre sí.
Estático o dinámico
La programación reactiva puede ser puramente estática cuando los flujos de datos se configuran estáticamente, o puede ser dinámica cuando los flujos de datos pueden cambiar durante la ejecución de un programa.
El uso de cambios de datos en el gráfico de flujo de datos podría, hasta cierto punto, hacer que un gráfico de flujo de datos estáticos parezca dinámico y difuminar ligeramente la distinción. Sin embargo, la verdadera programación reactiva dinámica podría utilizar la programación imperativa para reconstruir el gráfico de flujo de datos.
Programación reactiva de orden superior
Se podría decir que la programación reactiva es de orden superior si apoya la idea de que los flujos de datos podrían usarse para construir otros flujos de datos. Es decir, el valor resultante de un flujo de datos es otro gráfico de flujo de datos que se ejecuta utilizando el mismo modelo de evaluación que el primero.
Diferenciación del flujo de datos
Idealmente, todos los cambios de datos se propagan instantáneamente, pero esto no se puede garantizar en la práctica. En su lugar, podría ser necesario asignar diferentes prioridades de evaluación a diferentes partes del gráfico de flujo de datos. A esto se le puede llamar programación reactiva diferenciada . [4]
Por ejemplo, en un procesador de textos, la marcación de los errores ortográficos no tiene por qué estar totalmente sincronizada con la inserción de caracteres. En este caso, la programación reactiva diferenciada podría potencialmente usarse para dar al corrector ortográfico una prioridad más baja, permitiendo que se retrase mientras se mantienen otros flujos de datos instantáneos.
Sin embargo, tal diferenciación introduce una complejidad de diseño adicional. Por ejemplo, decidir cómo definir las diferentes áreas de flujo de datos y cómo manejar el paso de eventos entre diferentes áreas de flujo de datos.
Modelos de evaluación de programación reactiva
La evaluación de programas reactivos no se basa necesariamente en cómo se evalúan los lenguajes de programación basados en pilas. En cambio, cuando se modifican algunos datos, el cambio se propaga a todos los datos que se derivan parcial o completamente de los datos que se modificaron. Esta propagación del cambio podría lograrse de varias formas, donde quizás la forma más natural sea un esquema de invalidación / revalidación diferida.
Podría ser problemático simplemente propagar ingenuamente un cambio usando una pila, debido a la potencial complejidad exponencial de la actualización si la estructura de datos tiene una forma determinada. Una de esas formas se puede describir como "forma de diamantes repetidos", y tiene la siguiente estructura: A n → B n → A n + 1 , A n → C n → A n + 1 , donde n = 1,2 ... Este problema podría superarse propagando la invalidación solo cuando algunos datos aún no se hayan invalidado, y luego volver a validar los datos cuando sea necesario mediante la evaluación diferida .
Un problema inherente a la programación reactiva es que la mayoría de los cálculos que serían evaluados y olvidados en un lenguaje de programación normal, deben representarse en la memoria como estructuras de datos. [ cita requerida ] Esto podría potencialmente hacer que la programación reactiva consuma mucha memoria. Sin embargo, la investigación sobre lo que se llama reducción podría potencialmente superar este problema. [5]
Por otro lado, la programación reactiva es una forma de lo que podría describirse como "paralelismo explícito" [ cita requerida ] y, por lo tanto, podría ser beneficioso para utilizar el poder del hardware paralelo.
Similitudes con el patrón del observador
La programación reactiva tiene similitudes principales con el patrón de observador comúnmente utilizado en la programación orientada a objetos . Sin embargo, la integración de los conceptos de flujo de datos en el lenguaje de programación facilitaría su expresión y, por lo tanto, podría aumentar la granularidad del gráfico de flujo de datos. Por ejemplo, el patrón de observador comúnmente describe flujos de datos entre objetos / clases completos, mientras que la programación reactiva orientada a objetos podría apuntar a los miembros de objetos / clases.
Enfoques
Imperativo
Es posible fusionar la programación reactiva con la programación imperativa ordinaria. En tal paradigma, los programas imperativos operan sobre estructuras de datos reactivas. [6] Tal configuración es análoga a restringir la programación imperativa ; sin embargo, mientras que la programación imperativa de restricciones administra las restricciones bidireccionales, la programación imperativa reactiva administra las restricciones de flujo de datos unidireccionales.
Orientado a objetos
La programación reactiva orientada a objetos (OORP) es una combinación de programación orientada a objetos y programación reactiva. Quizás la forma más natural de hacer tal combinación es la siguiente: en lugar de métodos y campos, los objetos tienen reacciones que se reevalúan automáticamente cuando las otras reacciones de las que dependen se han modificado. [ cita requerida ]
Si un lenguaje OORP mantiene sus métodos imperativos, también entraría en la categoría de programación reactiva imperativa.
Funcional
La programación reactiva funcional (FRP) es un paradigma de programación para la programación reactiva en la programación funcional .
Basado en reglas
Una categoría relativamente nueva de lenguajes de programación utiliza restricciones (reglas) como concepto principal de programación. Consiste en reacciones a los eventos, que mantienen satisfechas todas las limitaciones. Esto no solo facilita las reacciones basadas en eventos, sino que hace que los programas reactivos sean fundamentales para la corrección del software. Un ejemplo de un lenguaje de programación reactivo basado en reglas es Ampersand, que se basa en el álgebra de relaciones . [7]
Implementaciones
- ReactiveX , una API para implementar programación reactiva con flujos, observables y operadores con implementaciones de múltiples lenguajes, incluidos RxJs, RxJava, .NET, RxPy y RxSwift.
- Elm (lenguaje de programación) Composición reactiva de la interfaz de usuario web.
- Flujos reactivos , un estándar de JVM para el procesamiento de flujo asíncrono con contrapresión sin bloqueo
- ObservableComputations , una implementación de .NET multiplataforma.
Ver también
- Observable (Computación) , observable en programación reactiva.
Referencias
- ↑ Trellis, Model-view-controller y el patrón de observador , Tele community.
- ^ "Incrustación de flujo de datos dinámico en un lenguaje de llamada por valor" . cs.brown.edu . Consultado el 9 de octubre de 2016 .
- ^ "Cruzando líneas de estado: adaptación de marcos orientados a objetos a lenguajes reactivos funcionales" . cs.brown.edu . Consultado el 9 de octubre de 2016 .
- ^ "Programación reactiva - El arte del servicio | La guía de gestión de TI" . theartofservice.com . Consultado el 2 de julio de 2016 .
- ^ Burchett, Kimberley; Cooper, Gregory H; Krishnamurthi, Shriram, "Reducción: una técnica de optimización estática para la reactividad funcional transparente", Actas del simposio 2007 ACM SIGPLAN sobre evaluación parcial y manipulación de programas basada en semántica (PDF) , págs. 71–80.
- ^ Demetrescu, Camil; Finocchi, Irene; Ribichini, Andrea, "Programación imperativa reactiva con restricciones de flujo de datos", Actas de la conferencia internacional ACM 2011 sobre lenguajes y aplicaciones de sistemas de programación orientados a objetos , págs. 407–26.
- ^ Joosten, Stef (2018), "Álgebra de relaciones como lenguaje de programación usando el compilador Ampersand", Journal of Logical and Algebraic Methods in Programming , 100 , pp. 113–29, doi : 10.1016 / j.jlamp.2018.04.002.
enlaces externos
- Una encuesta sobre programación reactiva Un artículo de 2013 de E. Bainomugisha, A. Lombide Carreton, T. Van Cutsem, S. Mostinckx y W. De Meuter que analiza y proporciona una taxonomía de los enfoques de programación reactiva existentes.
- Proyecto MIMOSA de INRIA - ENSMP , un sitio general sobre programación reactiva.
- Experimentando con Cells Demostración de una aplicación de programación reactiva simple en Lisp, usando la biblioteca Cells
- REScala Programación reactiva para aplicaciones OO.
- Deprecating the Observer Pattern Un artículo de 2010 de Ingo Maier, Tiark Rompf y Martin Odersky que describe un marco de programación reactiva para el lenguaje de programación Scala .
- Deprecating the Observer Pattern with Scala ( Reacción de un artículo de 2012 de Ingo)
- RxJS , la biblioteca de extensiones reactivas para "componer programas [...] asincrónicos usando secuencias observables"