Question Qu'est-ce que la programmation réactive (fonctionnelle)?


J'ai lu l'article Wikipedia sur programmation réactive. J'ai aussi lu le petit article sur programmation réactive fonctionnelle. Les descriptions sont assez abstraites.

  1. Que signifie la programmation fonctionnelle réactive (PRF) dans la pratique?
  2. Que signifie la programmation réactive (par opposition à la programmation non-réactive?)?

Mes antécédents sont dans les langues impératives / OO, donc une explication qui se rapporte à ce paradigme serait appréciée.


1149
2018-06-22 16:41


origine


Réponses:


Si vous voulez avoir une idée de FRP, vous pouvez commencer avec l'ancien Tutoriel Fran à partir de 1998, qui a des illustrations animées. Pour les papiers, commencez par Animation réactive fonctionnelle et ensuite suivre les liens sur le lien des publications sur ma page d'accueil et la FRP lien sur le Wiki de Haskell.

Personnellement, j'aime penser à ce que le FRP veux dire avant d'aborder comment cela pourrait être mis en œuvre. (Le code sans spécification est une réponse sans question et donc "même pas mal".) Donc je ne décris pas FRP dans les termes de représentation / implémentation comme le fait Thomas K dans une autre réponse (graphes, nœuds, bords, tir, exécution, etc.). Il existe de nombreux styles de mise en œuvre possibles, mais aucune implémentation ne dit quel FRP est.

Je résonne avec la description simple de Laurence G que FRP est sur "les types de données qui représentent une valeur" au fil du temps "". La programmation impérative conventionnelle capture ces valeurs dynamiques seulement indirectement, à travers l'état et les mutations. L'histoire complète (passé, présent, futur) n'a pas de représentation de première classe. De plus, seulement évoluant discrètement les valeurs peuvent être (indirectement) capturées, puisque le paradigme impératif est temporellement discret. En revanche, PRF capture ces valeurs évolutives directement et n'a pas de difficulté avec continuellement l'évolution des valeurs.

La FRP est également inhabituelle dans la mesure où elle est simultanée sans aller à l'encontre du nid des rats théoriques et pragmatiques qui pèse sur la concurrence impérative. Sémantiquement, la concurrence de FRP est à grain fin, déterminé, et continu. (Je parle de la signification, pas de l'implémentation Une implémentation peut ou non impliquer la concurrence ou le parallélisme.) La détermination sémantique est très importante pour le raisonnement, à la fois rigoureux et informel. Alors que la concurrence ajoute une énorme complexité à la programmation impérative (en raison de l'entrelacement non déterministe), elle est sans effort dans FRP.

Alors, qu'est-ce que le FRP? Tu aurais pu l'inventer toi-même. Commencez avec ces idées:

  • Les valeurs dynamiques / évolutives (c'est-à-dire les valeurs "dans le temps") sont des valeurs de première classe en elles-mêmes. Vous pouvez les définir et les combiner, les transmettre dans et hors des fonctions. J'ai appelé ces choses "comportements".

  • Les comportements sont construits à partir de quelques primitives, comme les comportements constants (statiques) et le temps (comme une horloge), puis avec une combinaison séquentielle et parallèle. n les comportements sont combinés en appliquant une fonction n-aire (sur des valeurs statiques), "point par point", c'est-à-dire, continuellement dans le temps.

  • Pour rendre compte des phénomènes discrets, avoir un autre type (famille) d '"événements", dont chacun a un flux (fini ou infini) d'occurrences. Chaque occurrence a un temps et une valeur associés.

  • Pour trouver le vocabulaire de composition à partir duquel tous les comportements et événements peuvent être construits, jouez avec quelques exemples. Continuez à vous déconstruire en morceaux plus généraux / simples.

  • Pour que vous sachiez que vous êtes sur une base solide, donnez à l'ensemble du modèle une base compositionnelle, en utilisant la technique de sémantique dénotationnelle, ce qui signifie simplement que (a) chaque type a un type mathématique simple et précis de "significations"; b) chaque primitive et opérateur a une signification simple et précise en fonction des significations des constituants. Plus jamais mélanger les considérations de mise en œuvre dans votre processus d'exploration. Si cette description vous chagrine, consultez (a) Conception dénotative avec des morphismes de classe de type, (b) Programmation réactive fonctionnelle push-pull (en ignorant les bits de mise en œuvre), et (c) le Sémantique dénotationnelle Page de wikibooks Haskell. Méfiez-vous que la sémantique dénotationnelle a deux parties, à partir de ses deux fondateurs Christopher Strachey et Dana Scott: la partie Strachey plus facile et plus utile et la partie Scott plus difficile et moins utile (pour la conception de logiciels).

Si vous respectez ces principes, je pense que vous obtiendrez quelque chose de plus ou moins dans l'esprit du FRP.

Où ai-je eu ces principes? Dans la conception de logiciels, je pose toujours la même question: "qu'est-ce que ça veut dire?". La sémantique dénotationnelle m'a donné un cadre précis pour cette question, et qui correspond à mon esthétique (contrairement à la sémantique opérationnelle ou axiomatique, qui me laissent insatisfait). Alors je me suis demandé quel est le comportement? Je me suis vite rendu compte que la nature temporellement discrète du calcul impératif est un accommodement à un style particulier de machine, plutôt qu'une description naturelle du comportement lui-même. La description précise la plus simple du comportement auquel je peux penser est simplement "fonction du temps (continu)", donc c'est mon modèle. Délicieusement, ce modèle gère la concurrence simultanée et déterministe avec facilité et grâce.

Il a été assez difficile de mettre en œuvre ce modèle correctement et efficacement, mais c'est une autre histoire.


932
2018-06-23 04:31



En programmation fonctionnelle pure, il n'y a pas d'effets secondaires. Pour de nombreux types de logiciels (par exemple, tout ce qui a une interaction avec l'utilisateur), des effets secondaires sont nécessaires à un certain niveau.

Une façon d'obtenir un comportement de type effet secondaire tout en conservant un style fonctionnel consiste à utiliser une programmation réactive fonctionnelle. C'est la combinaison de la programmation fonctionnelle et de la programmation réactive. (L'article de Wikipédia que vous avez lié est à propos de ce dernier.)

L'idée de base de la programmation réactive est qu'il existe certains types de données qui représentent une valeur "dans le temps". Les calculs qui impliquent ces valeurs changeantes dans le temps auront eux-mêmes des valeurs qui changent avec le temps.

Par exemple, vous pouvez représenter les coordonnées de la souris comme une paire de valeurs entier-sur-temps. Disons que nous avions quelque chose comme (c'est du pseudo-code):

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

A tout moment, x et y auraient les coordonnées de la souris. Contrairement à la programmation non-réactive, nous avons seulement besoin de faire cette affectation une fois, et les variables x et y resteront "à jour" automatiquement. C'est pourquoi la programmation réactive et la programmation fonctionnelle fonctionnent si bien ensemble: la programmation réactive supprime le besoin de muter des variables tout en vous permettant de faire beaucoup de choses que vous pourriez accomplir avec des mutations variables.

Si nous faisons ensuite quelques calculs basés sur ceci, les valeurs résultantes seront également des valeurs qui changent avec le temps. Par exemple:

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

Dans cet exemple, minX sera toujours inférieur de 16 à la coordonnée x du pointeur de la souris. Avec les bibliothèques réactives, vous pouvez alors dire quelque chose comme:

rectangle(minX, minY, maxX, maxY)

Et une boîte de 32x32 sera dessinée autour du pointeur de la souris et le suivra partout où il se déplace.

Voici un très bon document sur la programmation fonctionnelle réactive.


740
2018-06-22 18:06



Un moyen facile de parvenir à une première intuition à propos de ce que c'est de s'imaginer que votre programme est une feuille de calcul et toutes vos variables sont des cellules. Si l'une des cellules d'une feuille de calcul change, toutes les cellules qui se réfèrent à cette cellule changent également. C'est juste la même chose avec FRP. Maintenant, imaginez que certaines des cellules changent d'elles-mêmes (ou plutôt, sont prises du monde extérieur): dans une situation GUI, la position de la souris serait un bon exemple.

Cela passe nécessairement beaucoup trop loin. La métaphore se décompose assez rapidement lorsque vous utilisez réellement un système de PRF. Pour l'un, il y a généralement des tentatives de modélisation d'événements discrets (par exemple, la souris est cliquée). Je ne fais que mettre ceci ici pour vous donner une idée de ce que c'est.


144
2018-06-23 14:52



Pour moi, il s'agit de 2 significations différentes du symbole =:

  1. En maths x = sin(t) signifie que x est nom différent pour sin(t). Donc écrire x + y est la même chose que sin(t) + y. La programmation réactive fonctionnelle est comme les mathématiques à cet égard: si vous écrivez x + y, il est calculé avec quelle que soit la valeur de t est au moment où il est utilisé.
  2. Dans les langages de programmation de type C (langages impératifs), x = sin(t) est une mission: cela signifie que x stocke le valeur de  sin(t) pris au moment de la cession.

132
2018-05-25 14:52



OK, d'après les connaissances de base et la lecture de la page Wikipédia sur laquelle vous avez pointé, il semble que la programmation réactive ressemble au dataflow computing mais avec des "stimuli" externes spécifiques qui déclenchent un ensemble de nœuds pour effectuer leurs calculs.

Ceci est très bien adapté à la conception de l'interface utilisateur, par exemple, dans laquelle toucher un contrôle d'interface utilisateur (par exemple, le contrôle du volume sur une application de lecture musicale) peut avoir besoin de mettre à jour divers éléments d'affichage et le volume réel de sortie audio. Lorsque vous modifiez le volume (un curseur, disons) qui correspondrait à la modification de la valeur associée à un nœud dans un graphe orienté.

Divers nœuds ayant des arêtes provenant de ce nœud "valeur volumique" seraient automatiquement déclenchés et tous les calculs et mises à jour nécessaires se répercuteraient naturellement dans l'application. L'application "réagit" au stimulus de l'utilisateur. La programmation réactive fonctionnelle serait simplement la mise en œuvre de cette idée dans un langage fonctionnel, ou généralement dans un paradigme de programmation fonctionnelle.

Pour en savoir plus sur "l'informatique de flux de données", recherchez ces deux mots sur Wikipedia ou en utilisant votre moteur de recherche préféré. L'idée générale est la suivante: le programme est un graphe orienté de nœuds, chacun effectuant un calcul simple. Ces nœuds sont connectés les uns aux autres par des liens graphiques qui fournissent les sorties de certains nœuds aux entrées des autres.

Lorsqu'un nœud se déclenche ou effectue son calcul, les nœuds connectés à ses sorties ont leurs entrées correspondantes "déclenchées" ou "marquées". Tout nœud ayant toutes les entrées déclenchées / marquées / disponibles se déclenche automatiquement. Le graphique peut être implicite ou explicite en fonction de la manière exacte dont la programmation réactive est implémentée.

Les nœuds peuvent être considérés comme tirant en parallèle, mais ils sont souvent exécutés en série ou avec un parallélisme limité (par exemple, il peut y avoir quelques threads les exécutant). Un exemple célèbre était le Manchester Dataflow Machine, qui (IIRC) utilisait une architecture de données étiquetée pour planifier l'exécution de nœuds dans le graphique à travers une ou plusieurs unités d'exécution. Le calcul de flux de données est assez bien adapté aux situations dans lesquelles les calculs de déclenchement donnant lieu de façon asynchrone à des cascades de calculs fonctionnent mieux que d'essayer de faire régir l'exécution par une horloge (ou des horloges).

La programmation réactive importe cette idée de "cascade d'exécution" et semble penser au programme comme un flux de données mais à condition que certains des nœuds soient accrochés au "monde extérieur" et que les cascades d'exécution soient déclenchées lorsque ces fonctions sensorielles -comme les nœuds changent. L'exécution du programme ressemblerait alors à quelque chose d'analogue à un arc réflexe complexe. Le programme peut être ou non fondamentalement sessile entre les stimuli ou peut s'installer dans un état fondamentalement sessile entre les stimuli.

La programmation "non-réactive" serait une programmation avec une vision très différente du flux d'exécution et de la relation avec les intrants externes. Cela risque d'être quelque peu subjectif, car les gens seront probablement tentés de dire que tout ce qui répond aux intrants externes «réagit» à eux. Mais en regardant l'esprit de la chose, un programme qui interroge une file d'attente d'événements à un intervalle fixe et distribue tous les événements trouvés aux fonctions (ou threads) est moins réactif (car il ne s'occupe que de l'entrée utilisateur à un intervalle fixe). Encore une fois, c'est l'esprit de la chose ici: on peut imaginer mettre une implémentation d'interrogation avec un intervalle d'interrogation rapide dans un système à un niveau très bas et programmer de manière réactive par-dessus.


71
2018-06-22 17:45



Après avoir lu de nombreuses pages sur le FRP, j'ai finalement rencontré ce écrit éclairant sur le PRF, cela m'a finalement permis de comprendre ce qu'est vraiment le PRF.

Je cite ci-dessous Heinrich Apfelmus (auteur de la banane réactive).

Quelle est l'essence de la programmation réactive fonctionnelle?

Une réponse commune serait que "FRP est tout au sujet de décrire un système dans   termes de fonctions variant dans le temps au lieu d'un état mutable ", et que   n'aurait certainement pas tort. C'est le point de vue sémantique. Mais en   À mon avis, la réponse la plus profonde et la plus satisfaisante   suivant un critère purement syntaxique:

L'essence de la programmation réactive fonctionnelle est de spécifier le comportement dynamique d'une valeur complètement au moment de la déclaration.

Par exemple, prenons l'exemple d'un compteur: vous avez deux boutons   étiqueté "Up" et "Down" qui peut être utilisé pour incrémenter ou décrémenter   le compteur. Impérativement, vous devez d'abord spécifier une valeur initiale   puis changez-le chaque fois que vous appuyez sur un bouton; quelque chose comme ça:

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

Le fait est qu'au moment de la déclaration, seule la valeur initiale   pour le compteur est spécifié; le comportement dynamique du compteur est   implicite dans le reste du texte du programme. En revanche, fonctionnel   la programmation réactive spécifie l'ensemble du comportement dynamique à ce moment   de déclaration, comme ceci:

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

Chaque fois que vous voulez comprendre la dynamique du compteur, vous avez seulement   regarder sa définition. Tout ce qui peut lui arriver   apparaît sur le côté droit. Ceci est très en contraste avec le   approche impérative où les déclarations ultérieures peuvent changer la   comportement dynamique des valeurs précédemment déclarées.

Donc, dans ma compréhension un programme de PRF est un ensemble d'équations: enter image description here

j est discret: 1,2,3,4 ...

f dépend de t donc cela intègre la possibilité de modéliser des stimuli externes

tout l'état du programme est encapsulé dans des variables x_i

La bibliothèque FRP s'occupe de la progression du temps, en d'autres termes j à j+1.

J'explique ces équations de manière beaucoup plus détaillée dans ce vidéo.

MODIFIER:

Environ 2 ans après la réponse originale, je suis arrivé récemment à la conclusion que les implémentations de PRF ont un autre aspect important. Ils doivent (et habituellement) résoudre un problème pratique important: invalidation de cache.

Les équations pour x_i-s décrit un graphe de dépendance. Quand certains des x_i changements à l'heure j alors pas tous les autres x_i' valeurs à j+1 doivent être mis à jour, donc toutes les dépendances ne doivent pas être recalculées car certaines x_i' pourrait être indépendant de x_i.

En outre, x_i-s qui changent peuvent être mis à jour de manière incrémentielle. Par exemple, considérons une opération de carte f=g.map(_+1) à Scala, où f et g sont List de Ints. Ici f Correspond à x_i(t_j) et g est x_j(t_j). Maintenant, si je préfixer un élément à g alors il serait inutile de réaliser le map opération pour tous les éléments dans g. Certaines implémentations de PRF (par exemple reflex-frp) visent à résoudre ce problème. Ce problème est également connu sous le nom de calcul incrémental.

En d'autres termes, les comportements (les x_i-s) dans le PRF peut être considéré comme des calculs en cache. C'est la tâche du moteur FRP d'invalider et de recalculer efficacement ces cache-s (le x_i-s) si une partie de f_i-s font changer.


65
2018-01-31 03:46



Disclaimer: ma réponse est dans le contexte de rx.js - une bibliothèque de «programmation réactive» pour Javascript.

En programmation fonctionnelle, au lieu de parcourir chaque élément d'une collection, vous appliquez des fonctions d'ordre supérieur (HoF) à la collection elle-même. L'idée derrière FRP est donc qu'au lieu de traiter chaque événement individuel, créez un flux d'événements (implémenté avec un observable *) et appliquez des HoF à la place. De cette façon, vous pouvez visualiser le système comme des pipelines de données reliant les éditeurs aux abonnés.

Les principaux avantages de l'utilisation d'un observable sont:
i) il extrait l'état absent de votre code, par exemple, si vous voulez que le gestionnaire d'événements ne soit déclenché que pour chaque ième événement, ou arrête de tirer après les premiers 'n' événements, ou commence à tirer seulement après le premier 'événements, vous pouvez simplement utiliser les HoFs (filtre, takeUntil, skip respectivement) au lieu de définir, mettre à jour et vérifier les compteurs.
ii) il améliore la localité de code - si vous avez 5 gestionnaires d'événements différents changeant l'état d'un composant, vous pouvez fusionner leurs observables et définir un seul gestionnaire d'événement sur l'observable fusionné, combinant efficacement 5 gestionnaires d'événements en 1. Cela le rend très Il est facile de raisonner sur les événements de tout votre système qui peuvent affecter un composant, car ils sont tous présents dans un seul gestionnaire.

  • Un Observable est le dual d'un Iterable.

Un itérable est une séquence consommée paresseusement - chaque élément est tiré par l'itérateur quand il veut l'utiliser, et donc l'énumération est conduite par le consommateur.

Une observable est une séquence produite paresseusement - chaque élément est poussé à l'observateur chaque fois qu'il est ajouté à la séquence, et donc l'énumération est conduite par le producteur.


30
2018-05-26 17:10



Le papier Réactivité fonctionnelle simplement efficace par Conal Elliott (PDF direct, 233 KB) est une assez bonne introduction. La bibliothèque correspondante fonctionne également.

Le papier est maintenant remplacé par un autre papier, Programmation réactive fonctionnelle push-pull (PDF direct286 KB).


29
2018-06-22 17:48



Mec, c'est une idée géniale! Pourquoi n'ai-je pas découvert cela en 1998? Quoi qu'il en soit, voici mon interprétation de la Fran Didacticiel. Les suggestions sont les bienvenues, je pense à démarrer un moteur de jeu basé sur cela.

import pygame
from pygame.surface import Surface
from pygame.sprite import Sprite, Group
from pygame.locals import *
from time import time as epoch_delta
from math import sin, pi
from copy import copy

pygame.init()
screen = pygame.display.set_mode((600,400))
pygame.display.set_caption('Functional Reactive System Demo')

class Time:
    def __float__(self):
        return epoch_delta()
time = Time()

class Function:
    def __init__(self, var, func, phase = 0., scale = 1., offset = 0.):
        self.var = var
        self.func = func
        self.phase = phase
        self.scale = scale
        self.offset = offset
    def copy(self):
        return copy(self)
    def __float__(self):
        return self.func(float(self.var) + float(self.phase)) * float(self.scale) + float(self.offset)
    def __int__(self):
        return int(float(self))
    def __add__(self, n):
        result = self.copy()
        result.offset += n
        return result
    def __mul__(self, n):
        result = self.copy()
        result.scale += n
        return result
    def __inv__(self):
        result = self.copy()
        result.scale *= -1.
        return result
    def __abs__(self):
        return Function(self, abs)

def FuncTime(func, phase = 0., scale = 1., offset = 0.):
    global time
    return Function(time, func, phase, scale, offset)

def SinTime(phase = 0., scale = 1., offset = 0.):
    return FuncTime(sin, phase, scale, offset)
sin_time = SinTime()

def CosTime(phase = 0., scale = 1., offset = 0.):
    phase += pi / 2.
    return SinTime(phase, scale, offset)
cos_time = CosTime()

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    @property
    def size(self):
        return [self.radius * 2] * 2
circle = Circle(
        x = cos_time * 200 + 250,
        y = abs(sin_time) * 200 + 50,
        radius = 50)

class CircleView(Sprite):
    def __init__(self, model, color = (255, 0, 0)):
        Sprite.__init__(self)
        self.color = color
        self.model = model
        self.image = Surface([model.radius * 2] * 2).convert_alpha()
        self.rect = self.image.get_rect()
        pygame.draw.ellipse(self.image, self.color, self.rect)
    def update(self):
        self.rect[:] = int(self.model.x), int(self.model.y), self.model.radius * 2, self.model.radius * 2
circle_view = CircleView(circle)

sprites = Group(circle_view)
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
        if event.type == KEYDOWN and event.key == K_ESCAPE:
            running = False
    screen.fill((0, 0, 0))
    sprites.update()
    sprites.draw(screen)
    pygame.display.flip()
pygame.quit()

En bref: Si chaque composant peut être traité comme un nombre, tout le système peut être traité comme une équation mathématique, n'est-ce pas?


18
2018-03-13 09:44



Le livre de Paul Hudak, L'école d'expression Haskell, n'est pas seulement une bonne introduction à Haskell, mais il passe aussi beaucoup de temps sur FRP. Si vous êtes un débutant avec FRP, je le recommande fortement pour vous donner une idée du fonctionnement de FRP.

Il y a aussi ce qui ressemble à une nouvelle réécriture de ce livre (publié en 2011, mis à jour en 2014), L'école de musique Haskell.


14
2018-06-24 18:41