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 de cambios. 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 del flujo de datos modificado. [ cita requerida ]
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
pueden cambiarse sin que esto afecte al valor de a
. Por otro lado, en la programación reactivaa
, el valor de se actualiza automáticamente siempre que cambian los valores de b
o c
, sin que el programa tenga que volver a formular explícitamente la declaración a := b + c
para volver a asignar el valor de a
. [ cita requerida ]
var b = 1 var c = 2 var a = b + c b = 10 console.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 solo cuando se inicializa explícitamente, sino también cuando se cambian las variables referenciadas (en el lado derecho del operador ) var b = 1 var c = 2 var a $ = b + c b = 10 console.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 ]
Se ha propuesto la programación reactiva como una forma de simplificar la creación de interfaces de usuario interactivas y animaciones de sistemas casi en tiempo real. [ cita requerida ]
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]
En la creación de lenguajes de programación reactivos se emplean varios enfoques populares. Un enfoque es la especificación de lenguajes dedicados que son específicos para varias restricciones de dominio . Dichas restricciones generalmente se caracterizan por computación embebida en tiempo real o descripción de hardware. 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 embebidos específicos del dominio , que permiten la reactividad junto con el lenguaje de programación o sobre é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 la especificidad pueden dar como resultado el deterioro de la aplicabilidad general de un lenguaje.
La programación reactiva está regida por una variedad de modelos y semánticas. Podemos dividirlos libremente en las siguientes dimensiones:
Los tiempos de ejecución de los lenguajes de programación reactiva 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. Dichos tiempos de ejecución emplean dicho gráfico para ayudar a realizar un seguimiento de los diversos cálculos que deben ejecutarse nuevamente una vez que una entrada involucrada cambia de valor.
Los enfoques más comunes para la propagación de datos son:
En el nivel de implementación, la reacción a eventos consiste en la propagación a través de la información de un gráfico, 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 volver a ejecutarse. Dichos cálculos suelen caracterizarse por el cierre transitivo del cambio (es decir, el conjunto completo de dependencias transitivas 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 un 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 se propaga a lo largo de los bordes de un gráfico, que consisten solo en deltas que describen cómo se modificó 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 computación incremental , cuyo enfoque requiere la satisfacción del tiempo de ejecución que involucra el problema de actualización de vista . Este problema se caracteriza por el uso de entidades de base de datos , que son responsables del mantenimiento de 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 el lote y realizan modificaciones en consecuencia (por ejemplo, dos cambios en el lote pueden cancelarse entre sí y, por lo tanto, simplemente ignorarse). Otro enfoque disponible 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 :
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 el tiempo 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 la primera se evalúa antes que la segunda, entonces se cumplirá esta invariante. Sin embargo, si la 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 un error .
Algunos lenguajes reactivos no tienen fallos y demuestran esta propiedad [ cita requerida ] . Esto se logra generalmente ordenando topológicamente las expresiones y actualizando los valores en orden topológico. Sin embargo, esto puede tener implicaciones en el rendimiento, como demorar la entrega de valores (debido al orden de propagación). Por lo tanto, en algunos casos, los lenguajes reactivos permiten fallos y los desarrolladores deben ser conscientes de la posibilidad de que los valores no se correspondan temporalmente con la fuente del programa y de que algunas expresiones se evalúen varias veces (por ejemplo, pueden t > seconds
evaluarse dos veces: una cuando llega el nuevo valor seconds
y otra cuando t
se actualiza).
La ordenación topológica de las dependencias depende de que el gráfico de dependencias sea un gráfico acíclico dirigido (DAG). En la práctica, un programa puede definir un gráfico de dependencias que tenga ciclos. Por lo general, los lenguajes de programación reactiva 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. Por lo general, los lenguajes proporcionan un operador como delay
el que utiliza el mecanismo de actualización para este propósito, ya que a delay
implica que lo que sigue debe evaluarse en el "próximo paso de tiempo" (lo que permite que finalice la evaluación actual).
Los lenguajes reactivos suelen suponer 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 (lo que permite las 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 lograr que esta interacción sea fluida sigue siendo un problema abierto.
En algunos casos, es posible encontrar soluciones parciales basadas en principios. Dos de esas soluciones son:
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
es 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, que t + 1
luego depende de ella. Por lo 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 expresiones cada vez o mantener el nodo de una expresión construido pero inactivo; en este último caso, asegurarse de que no participen en el cálculo cuando no se supone que estén activos.
Los lenguajes de programación reactiva pueden ser muy explícitos, en los que los flujos de datos se configuran mediante flechas, o implícitos, en los que los flujos de datos se derivan de construcciones del lenguaje que parecen similares a las de la programación imperativa o funcional. Por ejemplo, en una programación reactiva funcional (FRP) implícitamente elevada, una llamada a una función puede provocar implícitamente la construcción de 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 del 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í.
La programación reactiva puede ser puramente estática, donde los flujos de datos se configuran de forma estática, o dinámica, donde los flujos de datos pueden cambiar durante la ejecución de un programa.
El uso de conmutadores de datos en el gráfico de flujo de datos podría hacer que un gráfico de flujo de datos estático parezca dinámico, y desdibujar ligeramente la distinción. Sin embargo, la programación reactiva verdaderamente 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 se pueden utilizar 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.
Lo ideal sería que todos los cambios de datos se propagaran instantáneamente, pero esto no se puede garantizar en la práctica. En cambio, podría ser necesario dar a las distintas partes del gráfico de flujo de datos diferentes prioridades de evaluación. Esto se puede llamar programación reactiva diferenciada . [ cita requerida ]
Por ejemplo, en un procesador de textos, la corrección de errores ortográficos no tiene por qué estar totalmente sincronizada con la inserción de caracteres. En este caso, se podría utilizar una programación reactiva diferenciada para dar una prioridad menor al corrector ortográfico, lo que permitiría retrasarlo y mantener instantáneos los demás flujos de datos.
Sin embargo, esta diferenciación introduce una complejidad de diseño adicional. Por ejemplo, hay que decidir cómo definir las distintas áreas de flujo de datos y cómo gestionar el paso de eventos entre distintas á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 pilas. En cambio, cuando se modifican algunos datos, el cambio se propaga a todos los datos que se derivan parcial o totalmente de los datos que se modificaron. Esta propagación del cambio se puede lograr 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 propagar un cambio de manera ingenua utilizando una pila, debido a la potencial complejidad exponencial de actualización si la estructura de datos tiene una forma determinada. Una de esas formas se puede describir como "forma de rombos 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 se podría superar propagando la invalidación solo cuando algunos datos no estén ya invalidados y, posteriormente, volviendo a validar los datos cuando sea necesario utilizando una evaluación diferida .
Un problema inherente a la programación reactiva es que la mayoría de los cálculos que se evaluarían y olvidarían en un lenguaje de programación normal, necesitan representarse en la memoria como estructuras de datos. [ cita requerida ] Esto podría hacer que la programación reactiva consuma mucha memoria. Sin embargo, la investigación sobre lo que se denomina 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 observador que se usa comúnmente 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 observador describe comúnmente 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 un paradigma de este tipo, los programas imperativos operan sobre estructuras de datos reactivas. [5] Esta configuración es análoga a la programación imperativa con restricciones ; sin embargo, mientras que la programación imperativa con restricciones gestiona restricciones de flujo de datos bidireccionales, la programación imperativa reactiva 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 es la siguiente: en lugar de métodos y campos, los objetos tienen reacciones que se reevalúan automáticamente cuando se modifican las otras reacciones de las que dependen. [ cita requerida ]
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 en la programación funcional .
Se ha propuesto que los actores diseñen sistemas reactivos, a menudo en combinación con la programación reactiva funcional (FRP) y los 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 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 . [10]