He leído el artículo de Wikipedia sobre programación reactiva . También leí el pequeño artículo sobre programación reactiva funcional . Las descripciones son bastante abstractas.
- ¿Qué significa la programación funcional reactiva (FRP) en la práctica?
- ¿En qué consiste la programación reactiva (en oposición a la programación no reactiva)?
Mi experiencia es en idiomas imperativos / OO, por lo que se agradecería una explicación que se relacione con este paradigma.
Respuestas:
Si desea familiarizarse con FRP, puede comenzar con el antiguo tutorial de Fran de 1998, que tiene ilustraciones animadas. Para los artículos, comience con la animación funcional reactiva y luego siga los enlaces en el enlace de publicaciones en mi página de inicio y el enlace FRP en el wiki de Haskell .
Personalmente, me gusta pensar en lo que significa FRP antes de abordar cómo podría implementarse. (El código sin una especificación es una respuesta sin una pregunta y, por lo tanto, "ni siquiera está mal"). Por lo tanto, no describo FRP en términos de representación / implementación como lo hace Thomas K en otra respuesta (gráficos, nodos, bordes, disparos, ejecución, etc) Hay muchos estilos de implementación posibles, pero ninguna implementación dice qué es FRP .
Resueno con la simple descripción de Laurence G de que FRP se trata de "tipos de datos que representan un valor 'en el tiempo'". La programación imperativa convencional captura estos valores dinámicos solo indirectamente, a través del estado y las mutaciones. La historia completa (pasado, presente, futuro) no tiene una representación de primera clase. Además, solo los valores que evolucionan discretamente pueden capturarse (indirectamente), ya que el paradigma imperativo es temporalmente discreto. En contraste, FRP captura estos valores en evolución directamente y no tiene dificultades con los valores en evolución continua .
El FRP también es inusual en el sentido de que es concurrente sin chocar con el nido de ratas teórico y pragmático que plaga la concurrencia imperativa. Semánticamente, la concurrencia de FRP es fina , determinada y continua . (Estoy hablando de significado, no de implementación. Una implementación puede o no implicar concurrencia o paralelismo). La determinación semántica es muy importante para el razonamiento, tanto riguroso como informal. Si bien la simultaneidad agrega una enorme complejidad a la programación imperativa (debido al entrelazado no determinista), es sin esfuerzo en FRP.
Entonces, ¿qué es FRP? Podrías haberlo inventado tú mismo. Comience con estas ideas:
Los valores dinámicos / en evolución (es decir, valores "a lo largo del tiempo") son valores de primera clase en sí mismos. Puede definirlos y combinarlos, pasarlos dentro y fuera de las funciones. Llamé a estas cosas "comportamientos".
Los comportamientos se forman a partir de unas pocas primitivas, como los comportamientos constantes (estáticos) y el tiempo (como un reloj), y luego con una combinación secuencial y paralela. n los comportamientos se combinan aplicando una función n-aria (en valores estáticos), "puntual", es decir, continuamente a lo largo del tiempo.
Para tener en cuenta los fenómenos discretos, tenga otro tipo (familia) de "eventos", cada uno de los cuales tiene una secuencia (finita o infinita) de ocurrencias. Cada ocurrencia tiene un tiempo y un valor asociados.
Para llegar al vocabulario compositivo a partir del cual se pueden construir todos los comportamientos y eventos, juegue con algunos ejemplos. Sigue deconstruyendo en piezas que sean más generales / simples.
Para que sepas que estás en terreno sólido, dale a todo el modelo una base compositiva, utilizando la técnica de semántica denotacional, lo que solo significa que (a) cada tipo tiene un tipo matemático de "significados" simple y preciso correspondiente, y ( b) cada primitivo y operador tiene un significado simple y preciso en función de los significados de los constituyentes. Nunca, nunca mezcle consideraciones de implementación en su proceso de exploración. Si esta descripción es un truco para usted, consulte (a) Diseño de denominación con morfismos de clase de tipo , (b) Programación reactiva funcional Push-pull (ignorando los bits de implementación), y (c) la página de wikilibros de Haskell de la semántica de denominación. Tenga en cuenta que la semántica denotacional tiene dos partes, de sus dos fundadores Christopher Strachey y Dana Scott: la parte de Strachey más fácil y más útil y la parte de Scott más difícil y menos útil (para el diseño de software).
Si sigue estos principios, espero que obtenga algo más o menos en el espíritu de FRP.
¿De dónde saqué estos principios? En el diseño de software, siempre hago la misma pregunta: "¿qué significa?". La semántica de denominación me dio un marco preciso para esta pregunta, y uno que se ajusta a mi estética (a diferencia de la semántica operativa o axiomática, las cuales me dejan insatisfecho). ¿Entonces me pregunté qué es el comportamiento? Pronto me di cuenta de que la naturaleza temporalmente discreta de la computación imperativa es una adaptación a un estilo particular de máquina , más que una descripción natural del comportamiento en sí. La descripción más simple y precisa del comportamiento que se me ocurre es simplemente "función del tiempo (continuo)", así que ese es mi modelo. Deliciosamente, este modelo maneja concurrencia continua y determinista con facilidad y gracia.
Implementar este modelo de manera correcta y eficiente ha sido todo un desafío, pero esa es otra historia.
fuente
En la programación funcional pura, no hay efectos secundarios. Para muchos tipos de software (por ejemplo, cualquier cosa con interacción del usuario), los efectos secundarios son necesarios en algún nivel.
Una forma de obtener un comportamiento similar a los efectos secundarios mientras se conserva un estilo funcional es utilizar la programación funcional reactiva. Esta es la combinación de programación funcional y programación reactiva. (El artículo de Wikipedia al que se vinculó es sobre este último).
La idea básica detrás de la programación reactiva es que hay ciertos tipos de datos que representan un valor "con el tiempo". Los cálculos que involucran estos valores cambiantes a lo largo del tiempo tendrán valores que cambiarán con el tiempo.
Por ejemplo, podría representar las coordenadas del mouse como un par de valores enteros en el tiempo. Digamos que teníamos algo así (esto es un pseudocódigo):
En cualquier momento, x e y tendrían las coordenadas del mouse. A diferencia de la programación no reactiva, solo necesitamos hacer esta asignación una vez, y las variables x e y se mantendrán "actualizadas" automáticamente. Esta es la razón por la cual la programación reactiva y la programación funcional funcionan tan bien juntas: la programación reactiva elimina la necesidad de mutar variables y al mismo tiempo le permite hacer mucho de lo que podría lograr con mutaciones variables.
Si luego hacemos algunos cálculos basados en esto, los valores resultantes también serán valores que cambian con el tiempo. Por ejemplo:
En este ejemplo,
minX
siempre será 16 menos que la coordenada x del puntero del mouse. Con las bibliotecas reactivas, puede decir algo como:Y se dibujará un cuadro de 32x32 alrededor del puntero del mouse y lo rastreará donde sea que se mueva.
Aquí hay un documento bastante bueno sobre programación reactiva funcional .
fuente
sqrt(x)
a C con su macro, eso solo calculasqrt(mouse_x())
y me devuelve un doble. En un verdadero sistema reactivo funcional,sqrt(x)
devolvería un nuevo "doble en el tiempo". Si intentara simular un sistema FR con#define
usted, tendría que renunciar a las variables a favor de las macros. Los sistemas FR también suelen recalcular las cosas cuando es necesario volver a calcularlas, mientras que el uso de macros significaría que estarías reevaluando constantemente todo, hasta las subexpresiones.Una manera fácil de llegar a una primera intuición sobre cómo es es imaginar que su programa es una hoja de cálculo y todas sus variables son celdas. Si alguna de las celdas de una hoja de cálculo cambia, las celdas que se refieren a esa celda también cambian. Es lo mismo con FRP. Ahora imagine que algunas de las celdas cambian por sí solas (o más bien, se toman del mundo exterior): en una situación de GUI, la posición del mouse sería un buen ejemplo.
Eso necesariamente se pierde bastante. La metáfora se descompone bastante rápido cuando realmente usa un sistema FRP. Por un lado, generalmente también hay intentos de modelar eventos discretos (por ejemplo, al hacer clic con el mouse). Solo pongo esto aquí para darte una idea de cómo es.
fuente
Para mí se trata de 2 significados diferentes de símbolo
=
:x = sin(t)
significa, esex
es un nombre diferente parasin(t)
. Entonces escribirx + y
es lo mismo quesin(t) + y
. La programación reactiva funcional es como las matemáticas a este respecto: si escribesx + y
, se calcula con el valor quet
sea en el momento en que se usa.x = sin(t)
es una asignación: significa quex
almacena el valor desin(t)
tomado en el momento de la asignación.fuente
x = sin(t)
mediosx
es el valor desin(t)
lo dadot
. Es no un nombre diferente parasin(t)
como la función. De lo contrario lo seríax(t) = sin(t)
.2 + 3 = 5
oa**2 + b**2 = c**2
.De acuerdo con los conocimientos previos y al leer la página de Wikipedia a la que apuntaste, parece que la programación reactiva es algo así como la computación de flujo de datos pero con "estímulos" externos específicos que activan un conjunto de nodos para disparar y realizar sus cálculos.
Esto es bastante adecuado para el diseño de la interfaz de usuario, por ejemplo, en el que tocar un control de interfaz de usuario (por ejemplo, el control de volumen en una aplicación de reproducción de música) podría necesitar actualizar varios elementos de visualización y el volumen real de salida de audio. Cuando modifica el volumen (un control deslizante, digamos) que correspondería a modificar el valor asociado con un nodo en un gráfico dirigido.
Varios nodos que tienen bordes de ese nodo de "valor de volumen" se activarán automáticamente y cualquier cálculo y actualización necesarios se moverán naturalmente por la aplicación. La aplicación "reacciona" al estímulo del usuario. La programación reactiva funcional sería simplemente la implementación de esta idea en un lenguaje funcional, o generalmente dentro de un paradigma de programación funcional.
Para obtener más información sobre "informática de flujo de datos", busque esas dos palabras en Wikipedia o utilice su motor de búsqueda favorito. La idea general es esta: el programa es un gráfico dirigido de nodos, cada uno de los cuales realiza un cálculo simple. Estos nodos están conectados entre sí mediante enlaces de gráficos que proporcionan las salidas de algunos nodos a las entradas de otros.
Cuando un nodo dispara o realiza su cálculo, los nodos conectados a sus salidas tienen sus entradas correspondientes "activadas" o "marcadas". Cualquier nodo que tenga todas las entradas activadas / marcadas / disponibles se dispara automáticamente. El gráfico puede ser implícito o explícito dependiendo exactamente de cómo se implementa la programación reactiva.
Los nodos pueden considerarse como disparos en paralelo, pero a menudo se ejecutan en serie o con un paralelismo limitado (por ejemplo, puede haber algunos subprocesos ejecutándolos). Un ejemplo famoso fue el Manchester Dataflow Machine , que (IIRC) utilizó una arquitectura de datos etiquetados para programar la ejecución de nodos en el gráfico a través de una o más unidades de ejecución. La computación de flujo de datos se adapta bastante bien a situaciones en las que disparar cálculos de forma asíncrona que da lugar a cascadas de cómputos funciona mejor que intentar que la ejecución se rija por un reloj (o relojes).
La programación reactiva importa esta idea de "cascada de ejecución" y parece pensar en el programa de una manera similar al flujo de datos, pero con la condición de que algunos de los nodos estén enganchados al "mundo exterior" y las cascadas de ejecución se activen cuando estos sensores -como los nodos cambian. La ejecución del programa se vería como algo análogo a un arco reflejo complejo. El programa puede o no ser básicamente sésil entre estímulos o puede establecerse en un estado básicamente sésil entre estímulos.
La programación "no reactiva" sería la programación con una visión muy diferente del flujo de ejecución y la relación con las entradas externas. Es probable que sea algo subjetivo, ya que las personas se verán tentadas a decir algo que responda a las entradas externas que "reaccionen" a ellas. Pero mirando el espíritu de la cosa, un programa que sondea una cola de eventos en un intervalo fijo y distribuye cualquier evento encontrado a funciones (o hilos) es menos reactivo (porque solo atiende la entrada del usuario en un intervalo fijo). Una vez más, aquí está el espíritu de la cosa: uno puede imaginar poner una implementación de sondeo con un intervalo de sondeo rápido en un sistema a un nivel muy bajo y programar de manera reactiva.
fuente
Después de leer muchas páginas sobre FRP, finalmente me encontré con este escrito esclarecedor sobre FRP, finalmente me hizo comprender de qué se trata realmente FRP.
Cito a continuación Heinrich Apfelmus (autor de plátano reactivo).
Entonces, en mi opinión, un programa FRP es un conjunto de ecuaciones:
j
es discreto: 1,2,3,4 ...f
depende det
lo que esto incorpora la posibilidad de modelar estímulos externostodo el estado del programa está encapsulado en variables
x_i
La biblioteca de FRP se encarga de tiempo progresa, es decir, teniendo
j
aj+1
.Explico estas ecuaciones con mucho más detalle en este video.
EDITAR:
Aproximadamente 2 años después de la respuesta original, recientemente llegué a la conclusión de que las implementaciones de FRP tienen otro aspecto importante. Necesitan (y generalmente lo hacen) resolver un problema práctico importante: la invalidación de caché .
Las ecuaciones para
x_i
-s describen un gráfico de dependencia. Cuando algunos de losx_i
cambios se realizan en el momentoj
, no es necesario actualizar todos los demásx_i'
valoresj+1
, por lo que no es necesario volver a calcular todas las dependencias, ya que algunasx_i'
podrían ser independientesx_i
.Además, los
x_i
-s que cambian pueden actualizarse gradualmente. Por ejemplo, consideremos una operación de mapaf=g.map(_+1)
en Scala, dondef
yg
sonList
deInts
. Aquíf
corresponde ax_i(t_j)
yg
esx_j(t_j)
. Ahora, si antepongo un elemento ag
, sería un desperdicio llevar a cabo lamap
operación para todos los elementos eng
. Algunas implementaciones de FRP (por ejemplo, reflex-frp ) tienen como objetivo resolver este problema. Este problema también se conoce como computación incremental.En otras palabras, los comportamientos (los
x_i
-s) en FRP pueden considerarse como cálculos en caché. Es tarea del motor FRP invalidar y volver a calcular de manera eficiente estos caché (sx_i
) si algunos de losf_i
cambios cambian.fuente
j+1
". En cambio, piense en las funciones del tiempo continuo. Como Newton, Leibniz y otros nos mostraron, a menudo es muy útil (y "natural" en un sentido literal) describir estas funciones de manera diferencial, pero continua, utilizando integrales y sistemas de EDO. De lo contrario, estás describiendo un algoritmo de aproximación (y uno pobre) en lugar de la cosa misma.El artículo Simplemente reactividad funcional eficiente de Conal Elliott ( PDF directo , 233 KB) es una introducción bastante buena. La biblioteca correspondiente también funciona.
El documento ahora está reemplazado por otro documento, la programación reactiva funcional Push-pull ( PDF directo , 286 KB).
fuente
Descargo de responsabilidad: mi respuesta está en el contexto de rx.js, una biblioteca de 'programación reactiva' para Javascript.
En la programación funcional, en lugar de recorrer cada elemento de una colección, aplica funciones de orden superior (HoFs) a la colección misma. Entonces, la idea detrás de FRP es que, en lugar de procesar cada evento individual, cree una secuencia de eventos (implementada con un * observable) y aplique HoFs a eso en su lugar. De esta manera, puede visualizar el sistema como canales de datos que conectan a los editores con los suscriptores.
Las principales ventajas de usar un observable son:
i) abstrae el estado de su código, por ejemplo, si desea que el controlador de eventos se dispare solo por cada 'enésimo evento, o deje de disparar después de los primeros' n 'eventos, o comience a disparar solo después de los primeros eventos 'n', puede usar los HoFs (filter, takeUntil, skip respectivamente) en lugar de configurar, actualizar y verificar contadores.
ii) mejora la localidad del código: si tiene 5 controladores de eventos diferentes que cambian el estado de un componente, puede fusionar sus observables y definir un solo controlador de eventos en el observable combinado, combinando efectivamente 5 controladores de eventos en 1. Esto lo hace muy Es fácil razonar sobre qué eventos en todo su sistema pueden afectar a un componente, ya que todo está presente en un solo controlador.
Un Iterable es una secuencia consumida perezosamente: cada elemento es extraído por el iterador cada vez que quiere usarlo y, por lo tanto, la enumeración es impulsada por el consumidor.
Un observable es una secuencia producida de manera perezosa: cada elemento se envía al observador cada vez que se agrega a la secuencia y, por lo tanto, la enumeración es impulsada por el productor.
fuente
¡Amigo, esta es una idea genial! ¿Por qué no me enteré de esto en 1998? De todos modos, aquí está mi interpretación del tutorial de Fran . Las sugerencias son bienvenidas, estoy pensando en iniciar un motor de juego basado en esto.
En resumen: si cada componente se puede tratar como un número, todo el sistema se puede tratar como una ecuación matemática, ¿verdad?
fuente
El libro de Paul Hudak, The Haskell School of Expression , no solo es una buena introducción a Haskell, sino que también pasa una buena cantidad de tiempo en FRP. Si es un principiante con FRP, lo recomiendo encarecidamente para darle una idea de cómo funciona FRP.
También existe lo que parece una nueva reescritura de este libro (publicado en 2011, actualizado en 2014), The Haskell School of Music .
fuente
Según las respuestas anteriores, parece que matemáticamente, simplemente pensamos en un orden superior. En lugar de pensar que un valor x tiene tipo X , pensamos en una función x : T → X , donde T es el tipo de tiempo, ya sean los números naturales, los enteros o el continuo. Ahora cuando escribimos y : = x + 1 en el lenguaje de programación, en realidad queremos decir la ecuación y ( t ) = x ( t ) + 1.
fuente
Actúa como una hoja de cálculo como se señaló. Por lo general, se basa en un marco basado en eventos.
Como con todos los "paradigmas", su novedad es discutible.
Según mi experiencia con las redes de actores de flujo distribuido, puede ser fácilmente presa de un problema general de consistencia de estado en la red de nodos, es decir, termina con mucha oscilación y atrapamiento en bucles extraños.
Esto es difícil de evitar ya que algunas semánticas implican bucles referenciales o transmisión, y puede ser bastante caótico ya que la red de actores converge (o no) en algún estado impredecible.
Del mismo modo, algunos estados pueden no alcanzarse, a pesar de tener bordes bien definidos, porque el estado global se aleja de la solución. 2 + 2 pueden o no llegar a ser 4 dependiendo de cuándo los 2 se convirtieron en 2 y si se mantuvieron así. Las hojas de cálculo tienen relojes síncronos y detección de bucles. Los actores distribuidos generalmente no lo hacen.
Todo muy divertido :).
fuente
Encontré este bonito video en el subreddit de Clojure sobre FRP. Es bastante fácil de entender incluso si no conoce Clojure.
Aquí está el video: http://www.youtube.com/watch?v=nket0K1RXU4
Aquí está la fuente a la que se refiere el video en la segunda mitad: https://github.com/Cicayda/yolk-examples/blob/master/src/yolk_examples/client/autocomplete.cljs
fuente
Este artículo de Andre Staltz es la mejor y más clara explicación que he visto hasta ahora.
Algunas citas del artículo:
Aquí hay un ejemplo de los diagramas fantásticos que forman parte del artículo:
fuente
Se trata de transformaciones de datos matemáticos a lo largo del tiempo (o ignorar el tiempo).
En código esto significa pureza funcional y programación declarativa.
Los errores estatales son un gran problema en el paradigma imperativo estándar. Varios bits de código pueden cambiar algún estado compartido en diferentes "momentos" en la ejecución de los programas. Esto es difícil de manejar.
En FRP, usted describe (como en la programación declarativa) cómo se transforman los datos de un estado a otro y qué los desencadena. Esto le permite ignorar el tiempo porque su función simplemente reacciona a sus entradas y usa sus valores actuales para crear una nueva. Esto significa que el estado está contenido en el gráfico (o árbol) de los nodos de transformación y es funcionalmente puro.
Esto reduce enormemente la complejidad y el tiempo de depuración.
Piense en la diferencia entre A = B + C en matemáticas y A = B + C en un programa. En matemáticas, estás describiendo una relación que nunca cambiará. En un programa, dice que "ahora mismo" A es B + C. Pero el siguiente comando podría ser B ++, en cuyo caso A no es igual a B + C. En matemáticas o programación declarativa, A siempre será igual a B + C, sin importar en qué momento lo solicite.
Entonces, eliminando las complejidades del estado compartido y cambiando los valores con el tiempo. Su programa es mucho más fácil de razonar.
Un EventStream es un EventStream + alguna función de transformación.
Un comportamiento es un EventStream + Algún valor en la memoria.
Cuando se activa el evento, el valor se actualiza ejecutando la función de transformación. El valor que esto produce se almacena en la memoria de comportamientos.
Los comportamientos se pueden componer para producir nuevos comportamientos que son una transformación en N otros comportamientos. Este valor compuesto se volverá a calcular a medida que se activen los eventos de entrada (comportamientos).
"Dado que los observadores no tienen estado, a menudo necesitamos varios de ellos para simular una máquina de estados como en el ejemplo de arrastre. Tenemos que guardar el estado donde sea accesible para todos los observadores involucrados, como en la ruta variable anterior".
Cita de - Deprecating The Observer Pattern http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf
fuente
La explicación breve y clara sobre la Programación reactiva aparece en Cyclejs - Programación reactiva , utiliza muestras simples y visuales.
Es un buen punto de inicio, no una fuente completa de conocimiento. A partir de ahí, podría saltar a papeles más complejos y profundos.
fuente
Echa un vistazo a Rx, Extensiones reactivas para .NET. Señalan que con IEnumerable básicamente estás 'tirando' de una transmisión. Las consultas de Linq sobre IQueryable / IEnumerable son operaciones de conjunto que 'succionan' los resultados de un conjunto. Pero con los mismos operadores sobre IObservable puede escribir consultas de Linq que 'reaccionen'.
Por ejemplo, podría escribir una consulta Linq como (desde m en MyObservableSetOfMouseMovements donde mX <100 y mY <100 seleccionan un nuevo Punto (mX, mY)).
y con las extensiones Rx, eso es todo: tiene un código de interfaz de usuario que reacciona al flujo entrante de movimientos del mouse y dibuja cada vez que se encuentra en el cuadro de 100,100 ...
fuente
FRP es una combinación de programación funcional (paradigma de programación construido sobre la idea de que todo es una función) y paradigma de programación reactiva (construido sobre la idea de que todo es una corriente (observador y filosofía observable)). Se supone que es el mejor de los mundos.
Para empezar, mira la publicación de Andre Staltz sobre programación reactiva.
fuente