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 (p. ej., matrices) o dinámicos (p. ej., 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 cambios. flujo de datos. [ cita necesaria ]
Por ejemplo, en una configuración de programación imperativaa := b + c
, significaría que a
se le asigna el resultado de b + c
en el instante en que se evalúa la expresión y, posteriormente, los valores de b
y c
se pueden cambiar sin ningún efecto sobre el valor de a
. Por otro lado, en la programación reactiva , el valor de a
se actualiza automáticamente cada vez que los valores de b
o c
cambian, sin que el programa tenga que volver a expresar explícitamente la declaración a := b + c
para reasignar el valor de a
. [ cita necesaria ]
var b = 1 var c = 2 var a = b + c b = 10 consola . log ( a ) // 3 (no 12 porque "=" no es un operador de asignación reactiva) // ahora imagina que tienes un operador especial "$=" que cambia el valor de una variable (ejecuta código en el lado derecho del operador y asigna el resultado a la variable del lado izquierdo) no sólo cuando se inicializa explícitamente, sino también cuando se hace referencia a variables ( en el lado derecho del operador) se cambian var b = 1 var c = 2 var a $ = b + c b = 10 consola . iniciar sesión ( 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 necesaria ]
La programación reactiva se ha propuesto como una forma de simplificar la creación de interfaces de usuario interactivas y animaciones del sistema casi en tiempo real. [ cita necesaria ]
Por ejemplo, en una arquitectura modelo-vista-controlador (MVC), la programación reactiva puede facilitar que los cambios en un modelo subyacente se reflejen automáticamente en una vista asociada . [1]
Se emplean varios enfoques populares en la creación de lenguajes de programación reactivos. Un enfoque es la especificación de lenguajes dedicados que sean específicos de varias restricciones de dominio . Estas restricciones suelen caracterizarse por una descripción de hardware o informática integrada en tiempo real. Otro enfoque implica la especificación de lenguajes de propósito general que incluyan soporte para la reactividad. Otros enfoques se articulan en la definición y el uso de bibliotecas de programación , o lenguajes integrados de dominio específico , que permiten la reactividad junto con el lenguaje de programación o además de él. La especificación y el uso de estos diferentes enfoques dan como resultado compensaciones en la capacidad del lenguaje . En general, cuanto más restringido es un lenguaje, más pueden sus compiladores y herramientas de análisis asociados informar a los desarrolladores (por ejemplo, al realizar análisis para determinar si los programas pueden ejecutarse en tiempo real). Las compensaciones funcionales en especificidad pueden resultar en el deterioro de la aplicabilidad general de un lenguaje.
Una variedad de modelos y semánticas gobiernan la programación reactiva. Podemos dividirlos libremente en las siguientes dimensiones:
Los tiempos de ejecución del lenguaje de programación reactivo se representan mediante un gráfico que identifica las dependencias entre los valores reactivos involucrados. En dicho gráfico, los nodos representan el acto de calcular y los bordes modelan las relaciones de dependencia. Dicho 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.
Los enfoques más comunes para la propagación de datos son:
A nivel de implementación, la reacción a un evento consiste en la propagación a través de la información de un gráfico, lo que caracteriza la existencia de un cambio. En consecuencia, los cálculos que se ven afectados por dicho cambio quedan obsoletos y deben marcarse para su reejecución. Dichos cálculos generalmente se caracterizan por el cierre transitivo del cambio (es decir, el conjunto completo de dependencias transitivas a las que afecta una fuente) en su fuente asociada. La propagación del cambio puede entonces conducir a una actualización en el 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, se ignora la salida anterior del nodo. Otro método implica la propagación delta , es decir, la propagación de cambios incrementales . En este caso, la información prolifera a lo largo de los bordes de un gráfico, que consisten únicamente en deltas 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ían costosos de volver a calcular desde cero.
La propagación delta es esencialmente una optimización que se ha estudiado ampliamente a través de la disciplina de la computación incremental , cuyo enfoque requiere la satisfacción del tiempo de ejecución que involucra el problema de visualización y actualización . Este problema se caracteriza notoriamente por el uso de entidades de bases 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. Luego se pueden emplear estrategias de optimización que razonan sobre la naturaleza de los cambios contenidos en ellos y realizar las modificaciones correspondientes. por ejemplo, dos cambios en el lote pueden cancelarse entre sí y, por tanto, simplemente ignorarse. Otro enfoque disponible se describe como propagación de notificaciones de nulidad . Este enfoque hace que los nodos con entradas no válidas obtengan actualizaciones, lo que resulta en la actualización de sus propias salidas.
Hay dos formas principales empleadas en la construcción de un gráfico de dependencia :
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 seconds
que es un valor reactivo que cambia cada segundo para representar la hora actual (en segundos). Considere esta expresión:
t = segundos + 1g = (t > segundos)
Como t
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, se deben actualizar dos expresiones: seconds + 1
y la condicional. Si el primero se evalúa antes que el segundo, entonces este invariante se mantendrá. Sin embargo, si el condicional se actualiza primero, utilizando el valor anterior de t
y el nuevo valor de seconds
, entonces la expresión se evaluará como un valor falso. Esto se llama falla .
Algunos lenguajes reactivos no tienen fallas y demuestran esta propiedad [ cita necesaria ] . Esto generalmente 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). Por lo tanto, en algunos casos, los lenguajes reactivos permiten fallas técnicas y los desarrolladores deben ser conscientes de la posibilidad de que los valores no correspondan temporalmente con el código fuente del programa y que algunas expresiones puedan evaluarse varias veces (por ejemplo, t > seconds
pueden evaluarse dos veces: una cuando el llega el nuevo valor de seconds
, y una vez más cuando t
se actualiza).
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 tenga ciclos. Por lo general, los lenguajes de programación reactivos esperan que dichos ciclos se "rompan" colocando algún elemento a lo largo de un "borde posterior" para permitir que finalice la actualización reactiva. Normalmente, los lenguajes proporcionan un operador como delay
el que utiliza el mecanismo de actualización para este propósito, ya que delay
implica que lo que sigue debe evaluarse en el "próximo paso" (permitiendo que finalice la evaluación actual).
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 actualizaciones y deje el orden específico sin especificar (lo que permite optimizaciones). Sin embargo, cuando un lenguaje reactivo está integrado en un lenguaje de programación con estado, los programadores pueden realizar 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:
En algunos lenguajes reactivos, el gráfico de dependencias es estático , es decir, el gráfico es fijo durante toda 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
hay un valor reactivo):
t= si ((segundos mod 2) == 0): segundos + 1 demás: segundos - 1 fint+1
Cada segundo, el valor de esta expresión cambia a una expresión reactiva diferente, de la que t + 1
luego depende. Por tanto, el gráfico de dependencias se actualiza cada segundo.
Permitir la actualización dinámica de dependencias proporciona un poder expresivo significativo (por ejemplo, las dependencias dinámicas ocurren rutinariamente en 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 este último caso, asegúrese de que no participen en el cálculo cuando se supone que no están activos.
Los lenguajes de programación reactivos pueden variar desde lenguajes muy explícitos en los que los flujos de datos se configuran mediante flechas, hasta lenguajes implícitos en los que los flujos de datos se derivan de construcciones de lenguaje que parecen similares a las de la programación imperativa o funcional. Por ejemplo, en la programación reactiva funcional (FRP) levantada implícitamente, una llamada a función podría 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, lo que permite que las especificaciones de flujo de datos sean implícitas y 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 comunes que se comunican entre sí.
La programación reactiva puede ser puramente estática donde los flujos de datos se configuran estáticamente, o dinámica donde los flujos de datos pueden cambiar durante la ejecución de un programa.
El uso de interruptores 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ático parezca dinámico y desdibujar 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.
Se podría decir que la programación reactiva es de orden superior si respalda 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.
Idealmente, todos los cambios de datos se propagan instantáneamente, pero esto no se puede garantizar en la práctica. En cambio, podría ser necesario asignar diferentes prioridades de evaluación a diferentes partes del gráfico de flujo de datos. Esto se puede llamar programación reactiva diferenciada . [ cita necesaria ]
Por ejemplo, en un procesador de textos, no es necesario que la corrección de errores ortográficos esté totalmente sincronizada con la inserción de caracteres. En este caso, la programación reactiva diferenciada podría usarse potencialmente para darle al corrector ortográfico una prioridad menor, permitiendo retrasarlo y manteniendo instantáneos otros flujos de datos.
Sin embargo, dicha 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.
La evaluación de programas reactivos no se basa necesariamente en cómo se evalúan los lenguajes de programación basados en pila. En cambio, cuando se cambian algunos datos, el cambio se propaga a todos los datos que se derivan parcial o completamente de los datos que se cambiaron. Esta propagación del cambio podría lograrse de varias maneras, donde quizás la forma más natural sea un esquema de invalidación/revalidación diferida.
Podría resultar problemático simplemente propagar ingenuamente un cambio utilizando una pila, debido a la posible complejidad exponencial de la actualización si la estructura de datos tiene una determinada forma. Una de esas formas puede describirse como "forma de diamante repetida" 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 estén invalidados y luego revalidando los datos cuando sea necesario mediante una 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 necesaria ] Esto podría 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. [4]
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 beneficiosa para utilizar el poder del hardware paralelo.
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, integrar los conceptos de flujo de datos en el lenguaje de programación facilitaría su expresión y, por 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.
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. [5] Tal configuración es análoga a la programación de restricciones imperativas ; sin embargo, mientras que la programación de restricciones imperativa gestiona restricciones de flujo de datos bidireccionales, la programación reactiva imperativa gestiona restricciones de flujo de datos unidireccionales. Una implementación de referencia es la extensión de tiempo de ejecución Quantum propuesta para JavaScript.
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 sea 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 han sido modificadas. [ cita necesaria ]
Si un lenguaje OORP mantiene sus métodos imperativos, también entraría en la categoría de programación reactiva imperativa.
La programación reactiva funcional (FRP) es un paradigma de programación para la programación reactiva sobre programación funcional .
Se han propuesto actores para diseñar sistemas reactivos, a menudo en combinación con programación reactiva funcional (FRP) y flujos reactivos para desarrollar sistemas reactivos distribuidos. [6] [7] [8] [9]
Una categoría relativamente nueva de lenguajes de programación utiliza restricciones (reglas) como concepto principal de programación. Consiste en reacciones a eventos, que mantienen satisfechas todas las restricciones. Esto no sólo 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 . [10]