Pregunta ¿Qué es la programación reactiva (funcional)?


He leído el artículo de Wikipedia sobre programación reactiva. También he leído el pequeño artículo sobre programación funcional reactiva. Las descripciones son bastante abstractas.

  1. ¿Qué significa la programación reactiva funcional (FRP) en la práctica?
  2. ¿En qué consiste la programación reactiva (en oposición a la programación no reactiva?)

Mi experiencia está en imperativo / OO idiomas, por lo que una explicación que se refiere a este paradigma sería apreciada.


1149
2018-06-22 16:41


origen


Respuestas:


Si quieres tener una idea de FRP, podrías comenzar con el viejo Tutorial de Fran desde 1998, que tiene ilustraciones animadas. Para documentos, comience con Animación reactiva funcional y luego seguir en los enlaces en el enlace de publicaciones en mi página de inicio y el FRP enlace en el Haskell wiki.

Personalmente, me gusta pensar qué FRP medio antes de abordar cómo se podría implementar. (El código sin una especificación es una respuesta sin una pregunta y, por lo tanto, "ni siquiera está mal"). Así que 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é FRP es.

Resueno con la descripción simple de Laurence G que FRP trata sobre "tipos de datos que representan un valor 'en el tiempo'". La programación imperativa convencional captura estos valores dinámicos solo de forma indirecta, a través del estado y las mutaciones. La historia completa (pasado, presente, futuro) no tiene representación de primera clase. Además, solo discretamente evolucionando los valores pueden ser (indirectamente) capturados, ya que el paradigma imperativo es temporalmente discreto. Por el contrario, FRP captura estos valores en evolución directamente y no tiene dificultad con continuamente valores en evolución.

FRP también es inusual en el sentido de que es concurrente sin entrar en conflicto con el nido de ratas teórico y pragmático que afecta a la concurrencia imperativa. Semánticamente, la concurrencia de FRP es de grano fino, determinadoy continuo. (Estoy hablando de significado, no de implementación. Una implementación puede o no implicar simultaneidad o paralelismo). La determinación semántica es muy importante para el razonamiento, tanto riguroso como informal. Mientras que la concurrencia agrega una enorme complejidad a la programación imperativa (debido al entrelazado no determinístico), es fácil en FRP.

Entonces, ¿qué es FRP? Podrías haberlo inventado tú mismo. Comience con estas ideas:

  • Los valores dinámicos / evolutivos (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 funciones. Llamé a estas cosas "comportamientos".

  • Los comportamientos se construyen a partir de unas pocas primitivas, como los comportamientos y el tiempo constantes (estáticos) (como un reloj) y luego con la combinación secuencial y paralela. norte los comportamientos se combinan aplicando una función n-aria (en valores estáticos), "puntualmente", 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 un flujo (finito o infinito) de ocurrencias. Cada aparición tiene un tiempo y valor asociados.

  • Para obtener el vocabulario de la composición a partir del cual se pueden construir todos los comportamientos y eventos, juegue con algunos ejemplos. Sigue deconstruyendo en piezas que son más generales / simples.

  • Para que sepas que estás en un terreno sólido, dale al modelo completo una base compositiva, usando la técnica de la semántica denotacional, lo que significa que (a) cada tipo tiene un correspondiente tipo matemático simple y preciso de "significados", y ( b) cada primitiva y operador tiene un significado simple y preciso en función de los significados de los constituyentes. Nunca jamás mezclar consideraciones de implementación en su proceso de exploración. Si esta descripción es un galimatías para usted, consulte (a) Diseño denotacional con morfismos de clase de tipo, (b) Push-pull funcional de programación reactiva (ignorando los bits de implementación), y (c) el Semántica Denotacional Página de wikibooks de Haskell. Tenga en cuenta que la semántica denotativa tiene dos partes, de sus dos fundadores Christopher Strachey y Dana Scott: la parte más fácil y más útil de Strachey y la parte de Scott más difícil y menos útil (para el diseño de software).

Si te apegas a estos principios, espero que obtengas algo más o menos en el espíritu de FRP.

¿De dónde obtuve estos principios? En el diseño de software, siempre hago la misma pregunta: "¿qué significa?". La semántica denotacional me dio un marco preciso para esta pregunta, y uno que se ajusta a mi estética (a diferencia de la semántica operacional o axiomática, que me dejan insatisfecho). Entonces me pregunté ¿qué es el comportamiento? Pronto me di cuenta de que la naturaleza temporal discreta de la computación imperativa es una adaptación a un estilo particular de máquina, en lugar de una descripción natural del comportamiento en sí mismo. La descripción más simple y precisa del comportamiento que puedo pensar es simplemente "función del tiempo (continuo)", así que ese es mi modelo. Deliciosamente, este modelo maneja concurrencia determinista continua con facilidad y gracia.

Ha sido todo un reto implementar este modelo de manera correcta y eficiente, pero esa es otra historia.


932
2018-06-23 04:31



En la programación funcional pura, no hay efectos secundarios. Para muchos tipos de software (por ejemplo, cualquier cosa con la interacción del usuario), los efectos colaterales son necesarios en algún nivel.

Una forma de obtener un comportamiento similar al efecto secundario sin perder un estilo funcional es utilizar programación reactiva funcional. Esta es la combinación de programación funcional y programación reactiva. (El artículo de Wikipedia al que se ha vinculado 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 "a lo largo del tiempo". Los cálculos que involucran estos valores de cambio en el tiempo tendrán valores que cambian con el tiempo.

Por ejemplo, podría representar las coordenadas del mouse como un par de valores enteros en el tiempo. Digamos que tenemos algo así como (esto es pseudocódigo):

x = <mouse-x>;
y = <mouse-y>;

En cualquier momento en el tiempo, xey 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 xey 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 al mismo tiempo que 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 cambien con el tiempo. Por ejemplo:

minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;

En este ejemplo, minX siempre será 16 menos que la coordenada x del puntero del mouse. Con bibliotecas conscientes de reactivos, podría decir algo como:

rectangle(minX, minY, maxX, maxY)

Y se dibujará un cuadro de 32x32 alrededor del puntero del mouse y lo rastreará donde sea que se mueva.

Aquí hay una muy buena documento sobre programación reactiva funcional.


740
2018-06-22 18:06



Una manera fácil de llegar a una primera intuición sobre cómo 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 hacen referencia a esa celda también cambian. Es lo mismo con FRP. Ahora imagine que algunas de las células cambian por sí solas (o, más bien, son tomadas del mundo exterior): en una situación de GUI, la posición del mouse sería un buen ejemplo.

Eso necesariamente falla bastante. La metáfora se descompone bastante rápido cuando realmente usas un sistema FRP. Por un lado, generalmente hay intentos de modelar eventos discretos también (por ejemplo, el clic del mouse). Solo estoy poniendo esto aquí para darte una idea de cómo es.


144
2018-06-23 14:52



Para mí, se trata de 2 significados diferentes de símbolo =:

  1. En matemáticas x = sin(t) significa que x es nombre diferente para sin(t). Entonces escribiendo x + y es lo mismo que sin(t) + y. La programación reactiva funcional es como las matemáticas a este respecto: si escribes x + y, se calcula con el valor de t es en el momento en que se usa.
  2. En lenguajes de programación similares a C (idiomas imperativos), x = sin(t) es una tarea: significa que x almacena el valor de  sin(t) tomado en el momento de la asignación.

132
2018-05-25 14:52



De acuerdo, desde el conocimiento de fondo y leyendo la página de Wikipedia a la que apunta, 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 disparan un conjunto de nodos para disparar y realizar sus cálculos.

Esto es muy adecuado para el diseño de IU, por ejemplo, en el que tocar el control de la 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 la modificación del valor asociado con un nodo en un gráfico dirigido.

Varios nodos que tienen bordes desde ese nodo de "valor de volumen" se activarán automáticamente y cualquier cálculo y actualización necesarios se propagarán de forma natural a través de 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 use su motor de búsqueda favorito. La idea general es la siguiente: el programa es un gráfico dirigido de nodos, cada uno realizando 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 disparará automáticamente. El gráfico puede ser implícito o explícito dependiendo de cómo se implementa la programación reactiva.

Los nodos se pueden considerar como disparos en paralelo, pero a menudo se ejecutan en serie o con un paralelismo limitado (por ejemplo, puede haber algunos hilos ejecutándolos). Un ejemplo famoso fue el Máquina de Dataflow de Manchester, que (IIRC) usó 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 informática de flujo de datos se adapta bastante bien a situaciones en las que los cálculos desencadenantes que generan asincrónicamente cascadas de cálculos funcionan 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 como un 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 desencadenen cuando estos sensores como los nodos cambian La ejecución del programa parecería análoga a un complejo arco reflejo. 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 probablemente se verán tentadas a decir que cualquier cosa que responda a las entradas externas "reacciona" ante ellas. Pero mirando el espíritu del asunto, un programa que sondea una cola de eventos en un intervalo fijo y distribuye cualquier evento encontrado a las funciones (o subprocesos) es menos reactivo (porque solo atiende a la entrada del usuario en un intervalo fijo). Una vez más, es el espíritu de lo que sucede aquí: uno puede imaginar la implementación de un sondeo con un intervalo de sondeo rápido en un sistema a un nivel muy bajo y programarlo de manera reactiva en la parte superior.


71
2018-06-22 17:45



Después de leer muchas páginas sobre FRP finalmente me encontré esta Escritura esclarecedora sobre FRP, finalmente me hizo entender de qué se trata realmente FRP.

Cito a continuación Heinrich Apfelmus (autor del plátano reactivo).

¿Cuál es la esencia de la programación reactiva funcional?

Una respuesta común sería que "FRP se trata de describir un sistema en   términos de funciones variables en el tiempo en lugar de estado mutable ", y eso   ciertamente no estaría mal. Este es el punto de vista semántico. Pero en   mi opinión, la respuesta más profunda, más satisfactoria es dada por el   siguiendo el criterio puramente sintáctico:

La esencia de la programación reactiva funcional es especificar el comportamiento dinámico de un valor por completo en el momento de la declaración.

Por ejemplo, tome el ejemplo de un contador: tiene dos botones   etiquetados "Arriba" y "Abajo" que se pueden usar para aumentar o disminuir   el contador. Imperativamente, primero debe especificar un valor inicial   y luego cámbielo siempre que se presione un botón; algo como esto:

counter := 0                               -- initial value
on buttonUp   = (counter := counter + 1)   -- change it later
on buttonDown = (counter := counter - 1)

El punto es que en el momento de la declaración, solo el valor inicial   para el contador está especificado; el comportamiento dinámico del contador es   implícito en el resto del texto del programa. Por el contrario, funcional   la programación reactiva especifica todo el comportamiento dinámico en el momento   de declaración, así:

counter :: Behavior Int
counter = accumulate ($) 0
            (fmap (+1) eventUp
             `union` fmap (subtract 1) eventDown)

Cuando quiera comprender la dinámica del contador, solo tiene   para ver su definición. Todo lo que pueda sucederle lo hará   aparecer en el lado derecho. Esto está muy en contraste con el   enfoque imperativo donde las declaraciones posteriores pueden cambiar el   Comportamiento dinámico de valores previamente declarados.

Entonces, en mi entendimiento un programa FRP es un conjunto de ecuaciones: enter image description here

j es discreto: 1,2,3,4 ...

f depende de t entonces esto incorpora la posibilidad de modelar estímulos externos

todo el estado del programa está encapsulado en variables x_i

La biblioteca de FRP se ocupa del tiempo progresivo, en otras palabras, tomar j a j+1.

Explico estas ecuaciones con mucho más detalle en esta vídeo.

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 importante problema práctico: invalidación de caché.

Las ecuaciones para x_i-s describen un gráfico de dependencia. Cuando algunas de las x_i cambios en el tiempo j entonces no todos los demás x_i' valores en j+1 necesita ser actualizado, por lo que no todas las dependencias deben ser recalculadas porque algunos x_i' podría ser independiente de x_i.

Además, x_i-s que cambian se pueden actualizar incrementalmente. Por ejemplo, consideremos una operación de mapa f=g.map(_+1) en Scala, donde f y g son List de Ints. aquí f corresponde a x_i(t_j) y g es x_j(t_j). Ahora si antepongo un elemento a g entonces sería un desperdicio llevar a cabo la map operación para todos los elementos en g. Algunas implementaciones de FRP (por ejemplo reflejo-frp) pretenden resolver este problema Este problema también se conoce como informática incremental.

En otras palabras, comportamientos (el x_i-s) en FRP se puede considerar como cálculos almacenados en caché. La tarea del motor de FRP es invalidar y recalcular eficazmente estos cachés (el x_i-s) si algunos de los f_i-s cambian


65
2018-01-31 03:46



Descargo de responsabilidad: mi respuesta se encuentra en el contexto de rx.js, una biblioteca de "programación reactiva" para Javascript.

En la programación funcional, en lugar de iterar a través de 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 un flujo de eventos (implementado con un observable *) y aplique HoFs a eso en su lugar. De esta forma, puede visualizar el sistema como un canal de datos que conecta 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 sea despedido solo por cada 'n'º evento, o deje de disparar después de los primeros' n 'eventos, o comience a disparar solo después del primer' n ' 'eventos, puede usar los HoFs (filtro, takeUntil, saltar, respectivamente) en lugar de configurar, actualizar y verificar contadores.
ii) mejora la ubicación del código: si tienes 5 manejadores de eventos diferentes que cambian el estado de un componente, puedes fusionar sus observables y definir un único manejador de eventos en el observable combinado, combinando efectivamente 5 manejadores de eventos en 1. Esto lo hace muy Es fácil razonar sobre qué eventos en su sistema completo pueden afectar a un componente, ya que todo está presente en un único controlador.

  • Un Observable es el doble de un Iterable.

Una Iterable es una secuencia consumida perezosamente: cada elemento es arrastrado por el iterador cada vez que quiere usarlo, y por lo tanto, la enumeración es impulsada por el consumidor.

Una secuencia observable es producida de forma lenta: 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.


30
2018-05-26 17:10