Programmation réactive - Reactive programming

En informatique , la programmation réactive est un paradigme de programmation déclarative concerné par les flux de données et la propagation du changement. Avec ce paradigme, il est possible d'exprimer facilement des flux de données statiques (par exemple, des tableaux) ou dynamiques (par exemple, des émetteurs d'événements) , et également de communiquer qu'une dépendance inférée au sein du modèle d'exécution associé existe, ce qui facilite la propagation automatique des données modifiées. couler.

Par exemple, dans un paramètre de programmation impérative , cela signifierait que le résultat de est affecté à l'instant où l'expression est évaluée, et plus tard, les valeurs de et peuvent être modifiées sans effet sur la valeur de . D'autre part, dans la programmation réactive , la valeur de est automatiquement mise à jour chaque fois que les valeurs de ou changent, sans que le programme ait à réexécuter explicitement l'instruction pour déterminer la valeur actuellement attribuée de

var b = 1
var c = 2
var a = b + c
b = 10
console.log(a) // 3 (not 12 because "=" is not a reactive assignment operator)

// now imagine you have a special operator "$=" that changes the value of a variable (executes code on the right side of the operator and assigns result to left side variable) not only when explicitly initialized, but also when referenced variables (on the right side of the operator) are changed
var b = 1
var c = 2
var a $= b + c
b = 10
console.log(a) // 12

Un autre exemple est un langage de description de matériel tel que Verilog , où la programmation réactive permet de modéliser les changements au fur et à mesure qu'ils se propagent dans les circuits.

La programmation réactive a été proposée comme un moyen de simplifier la création d'interfaces utilisateur interactives et l'animation du système en temps quasi réel.

Par exemple, dans une architecture modèle-vue-contrôleur (MVC), la programmation réactive peut faciliter les modifications d'un modèle sous-jacent qui se reflètent automatiquement dans une vue associée .

Approches pour créer des langages de programmation réactifs

Plusieurs approches populaires sont utilisées dans la création de langages de programmation réactifs. Spécification de langages dédiés spécifiques aux différentes contraintes du domaine . De telles contraintes sont généralement caractérisées par une description du matériel ou de l'informatique embarquée en temps réel. Une autre approche implique la spécification de langages à usage général qui incluent la prise en charge de la réactivité. D'autres approches sont articulées dans la définition et l'utilisation de bibliothèques de programmation , ou de langages embarqués spécifiques à un domaine , qui permettent une réactivité parallèlement ou en plus du langage de programmation. La spécification et l'utilisation de ces différentes approches aboutissent à des compromis sur les capacités linguistiques . En général, plus un langage est restreint, plus ses compilateurs et outils d'analyse associés sont capables d'informer les développeurs (par exemple, en effectuant une analyse pour savoir si les programmes sont capables de s'exécuter en temps réel). Les compromis fonctionnels dans la spécificité peuvent entraîner une détérioration de l'applicabilité générale d'une langue.

Modèles de programmation et sémantique

Une variété de modèles et de sémantiques régissent la famille de la programmation réactive. Nous pouvons les diviser grossièrement selon les dimensions suivantes :

  • Synchrony : le modèle sous-jacent du temps est-il synchrone versus asynchrone ?
  • Déterminisme : déterministe versus non déterministe dans le processus d'évaluation et les résultats
  • Processus de mise à jour : rappels contre flux de données contre acteur

Techniques de mise en œuvre et défis

Essence des implémentations

Les runtimes de langage de programmation réactif sont représentés par un graphique qui identifie les dépendances parmi les valeurs réactives impliquées. Dans un tel graphe, les nœuds représentent l'acte de calcul et les arêtes modélisent les relations de dépendance. Un tel runtime utilise ledit graphe, pour l'aider à garder une trace des différents calculs, qui doivent être exécutés à nouveau, une fois qu'une entrée impliquée change de valeur.

Changer les algorithmes de propagation

Les approches les plus courantes pour la propagation des données sont :

  • Pull : Le consommateur de valeur est en effet proactif , en ce sens qu'il interroge régulièrement la source de valeurs observée et réagit dès qu'une valeur pertinente est disponible. Cette pratique consistant à vérifier régulièrement les événements ou les changements de valeur est communément appelée interrogation .
  • Push : Le consommateur de valeur reçoit une valeur de la source chaque fois que la valeur devient disponible. Ces valeurs sont autonomes, c'est-à-dire qu'elles contiennent toutes les informations nécessaires et aucune autre information n'a besoin d'être interrogée par le consommateur.
  • Push-pull : Le consommateur de valeur reçoit une notification de changement , qui est une brève description du changement, par exemple "une valeur modifiée" - c'est la partie push . Cependant, la notification ne contient pas toutes les informations nécessaires (c'est-à-dire ne contient pas les valeurs réelles), donc le consommateur doit interroger la source pour plus d'informations (la valeur spécifique) après avoir reçu la notification - c'est la partie pull . Cette méthode est couramment utilisée lorsqu'il existe un grand volume de données susceptibles d'intéresser les consommateurs. Ainsi, afin de réduire le débit et la latence, seules des notifications légères sont envoyées ; et ensuite, les consommateurs qui ont besoin de plus d'informations demanderont ces informations spécifiques. Cette approche présente également l'inconvénient que la source peut être submergée par de nombreuses demandes d'informations supplémentaires après l'envoi d'une notification.

Que pousser ?

Au niveau de la mise en œuvre, la réaction à l'événement consiste en la propagation à travers les informations d'un graphe, qui caractérise l'existence d'un changement. Par conséquent, les calculs qui sont affectés par un tel changement deviennent alors obsolètes et doivent être signalés pour une réexécution. De tels calculs sont alors généralement caractérisés par la fermeture transitive du changement dans sa source associée. La propagation du changement peut alors conduire à une mise à jour de la valeur des puits du graphe .

Les informations propagées par le graphe peuvent consister en l'état complet d'un nœud, c'est-à-dire le résultat du calcul du nœud concerné. Dans de tels cas, la sortie précédente du nœud est alors ignorée. Une autre méthode implique la propagation delta, c'est-à-dire la propagation des changements incrémentiels . Dans ce cas, les informations prolifèrent le long des bords d' un graphique , qui ne sont constitués que de deltas décrivant comment le nœud précédent a été modifié. Cette approche est particulièrement importante lorsque les nœuds contiennent de grandes quantités de données d'état , qui seraient autrement coûteuses à recalculer à partir de zéro.

La propagation delta est essentiellement une optimisation qui a été largement étudiée via la discipline de l' informatique incrémentale , dont l'approche nécessite une satisfaction à l'exécution impliquant le problème de mise à jour de la vue . Ce problème est tristement célèbre par l'utilisation d' entités de base de données , qui sont responsables de la maintenance des vues de données changeantes.

Une autre optimisation courante est l'utilisation de l' accumulation de changements unaires et de la propagation par lots . Une telle solution peut être plus rapide car elle réduit la communication entre les nœuds impliqués. Des stratégies d'optimisation peuvent ensuite être employées pour raisonner sur la nature des changements contenus à l'intérieur et apporter des modifications en conséquence. Par exemple, deux changements dans le lot peuvent s'annuler et ainsi être simplement ignorés. Encore une autre approche disponible, est décrite comme la propagation de notification d'invalidité . Cette approche amène les nœuds avec une entrée invalide à extraire des mises à jour, entraînant ainsi la mise à jour de leurs propres sorties.

Il y a deux manières principales employées dans la construction d'un graphe de dépendance :

  1. Le graphe des dépendances est maintenu implicitement dans une boucle d'événements . L'enregistrement des rappels explicites, entraîne alors la création de dépendances implicites. Par conséquent, l' inversion de contrôle , qui est induite via le rappel, est ainsi laissée en place. Cependant, rendre les rappels fonctionnels (c'est-à-dire renvoyer une valeur d'état au lieu d'une valeur unitaire) nécessite que ces rappels deviennent compositionnels.
  2. Un graphe de dépendances est spécifique au programme et généré par un programmeur. Cela facilite un adressage de l' inversion de contrôle du rappel de deux manières : soit un graphe est spécifié explicitement (généralement en utilisant un langage spécifique au domaine (DSL), qui peut être intégré), soit un graphe est défini implicitement avec expression et génération à l'aide d'un , langage archétypal .

Défis de mise en œuvre dans la programmation réactive

Défaillance

Lors de la propagation des modifications, il est possible de choisir des ordres de propagation tels que la valeur d'une expression ne soit pas une conséquence naturelle du programme source. Nous pouvons illustrer cela facilement avec un exemple. Supposons qu'il s'agisse d' secondsune valeur réactive qui change toutes les secondes pour représenter l'heure actuelle (en secondes). Considérez cette expression :

t = seconds + 1
g = (t > seconds)
Programmation réactive glitchs.svg

Parce qu'elle tdoit toujours être supérieure à seconds, cette expression doit toujours être évaluée à une valeur vraie. Malheureusement, cela peut dépendre de l'ordre d'évaluation. En cas de secondschangement, deux expressions doivent être mises à jour : seconds + 1et le conditionnel. Si le premier évalue avant le second, alors cet invariant sera vérifié. Si, cependant, la conditionnelle est mise à jour en premier, en utilisant l'ancienne valeur de tet la nouvelle valeur de seconds, alors l'expression sera évaluée à une valeur fausse. C'est ce qu'on appelle un pépin .

Certains langages réactifs sont sans glitch et prouvent cette propriété. Ceci est généralement réalisé en triant topologiquement les expressions et en mettant à jour les valeurs dans l'ordre topologique. Cela peut cependant avoir des implications sur les performances, telles que le retard de la livraison des valeurs (en raison de l'ordre de propagation). Dans certains cas, par conséquent, les langages réactifs permettent des problèmes, et les développeurs doivent être conscients de la possibilité que les valeurs peuvent temporairement ne pas correspondre à la source du programme, et que certaines expressions peuvent être évaluées plusieurs fois (par exemple, t > secondspeuvent être évaluées deux fois : une fois lorsque le nouvelle valeur de secondsarrive, et encore une fois lors des tmises à jour).

Dépendances cycliques

Le tri topologique des dépendances dépend du fait que le graphe de dépendances est un graphe acyclique orienté (DAG). En pratique, un programme peut définir un graphe de dépendance qui a des cycles. Habituellement, les langages de programmation réactifs s'attendent à ce que de tels cycles soient "brisés" en plaçant un élément le long d'un "bord arrière" pour permettre à la mise à jour réactive de se terminer. Typiquement, les langages fournissent un opérateur comme delaycelui utilisé par le mécanisme de mise à jour à cette fin, car a delayimplique que ce qui suit doit être évalué dans le « prochain pas de temps » (permettant à l'évaluation en cours de se terminer).

Interaction avec l'état mutable

Les langages réactifs supposent généralement que leurs expressions sont purement fonctionnelles . Cela permet à un mécanisme de mise à jour de choisir différents ordres dans lesquels effectuer les mises à jour, et de laisser l'ordre spécifique non spécifié (permettant ainsi des optimisations). Cependant, lorsqu'un langage réactif est intégré dans un langage de programmation avec état, il peut être possible pour les programmeurs d'effectuer des opérations mutables. Comment rendre cette interaction fluide reste un problème ouvert.

Dans certains cas, il est possible d'avoir des solutions partielles de principe. Deux de ces solutions incluent :

  • Un langage peut proposer une notion de "cellule mutable". Une cellule mutable est une cellule dont le système de mise à jour réactive est conscient, de sorte que les modifications apportées à la cellule se propagent au reste du programme réactif. Cela permet à la partie non réactive du programme d'effectuer une mutation traditionnelle tout en permettant au code réactif d'être au courant et de répondre à cette mise à jour, maintenant ainsi la cohérence de la relation entre les valeurs dans le programme. Un exemple de langage réactif qui fournit une telle cellule est FrTime.
  • Les bibliothèques orientées objet correctement encapsulées offrent une notion d'état encapsulée. En principe, il est donc possible pour une telle bibliothèque d'interagir en douceur avec la partie réactive d'un langage. Par exemple, des rappels peuvent être installés dans les getters de la bibliothèque orientée objet pour informer le moteur de mise à jour réactive des changements d'état, et les modifications du composant réactif peuvent être transmises à la bibliothèque orientée objet via des getters. FrTime utilise une telle stratégie.

Mise à jour dynamique du graphe des dépendances

Dans certains langages réactifs, le graphe des dépendances est statique , c'est-à-dire que le graphe est figé tout au long de l'exécution du programme. Dans d'autres langages, le graphe peut être dynamique , c'est-à-dire qu'il peut changer au fur et à mesure de l'exécution du programme. Pour un exemple simple, considérons cet exemple illustratif (où secondsest une valeur réactive) :

t =
  if ((seconds mod 2) == 0):
    seconds + 1
  else:
    seconds - 1
  end
t + 1

Chaque seconde, la valeur de cette expression change en une expression réactive différente, qui t + 1dépend alors de. Par conséquent, le graphique des dépendances est mis à jour toutes les secondes.

Permettre la mise à jour dynamique des dépendances fournit une puissance expressive significative (par exemple, des dépendances dynamiques se produisent régulièrement dans les programmes d' interface utilisateur graphique (GUI)). Cependant, le moteur de mise à jour réactif doit décider s'il faut reconstruire les expressions à chaque fois, ou garder le nœud d'une expression construit mais inactif ; dans ce dernier cas, s'assurer qu'ils ne participent pas au calcul alors qu'ils ne sont pas censés être actifs.

notions

Degrés d'explicitation

Les langages de programmation réactifs peuvent aller de langages très explicites où les flux de données sont configurés à l'aide de flèches, à implicites où les flux de données sont dérivés de constructions de langage qui ressemblent à celles de la programmation impérative ou fonctionnelle. Par exemple, dans la programmation réactive fonctionnelle (FRP) implicitement levée, un appel de fonction peut implicitement provoquer la construction d'un nœud dans un graphe de flux de données. Les bibliothèques de programmation réactives pour les langages dynamiques (telles que les bibliothèques Lisp "Cells" et Python "Trellis") peuvent construire un graphe de dépendances à partir de l'analyse d'exécution des valeurs lues pendant l'exécution d'une fonction, permettant aux spécifications de flux de données d'être à la fois implicites et dynamiques.

Parfois, le terme programmation réactive fait référence au niveau architectural du génie logiciel, où les nœuds individuels dans le graphe de flux de données sont des programmes ordinaires qui communiquent entre eux.

Statique ou dynamique

La programmation réactive peut être purement statique lorsque les flux de données sont configurés de manière statique, ou dynamique lorsque les flux de données peuvent changer au cours de l'exécution d'un programme.

L'utilisation de commutateurs de données dans le graphique de flux de données pourrait dans une certaine mesure faire apparaître un graphique de flux de données statique comme dynamique et brouiller légèrement la distinction. Une véritable programmation réactive dynamique pourrait cependant utiliser une programmation impérative pour reconstruire le graphe de flux de données.

Programmation réactive d'ordre supérieur

On pourrait dire que la programmation réactive est d' un ordre supérieur si elle soutient l'idée que les flux de données pourraient être utilisés pour construire d'autres flux de données. C'est-à-dire que la valeur résultante d'un flux de données est un autre graphique de flux de données qui est exécuté en utilisant le même modèle d'évaluation que le premier.

Différenciation des flux de données

Idéalement, toutes les modifications de données sont propagées instantanément, mais cela ne peut pas être assuré dans la pratique. Au lieu de cela, il peut être nécessaire de donner différentes priorités d'évaluation à différentes parties du graphique de flux de données. Cela peut être appelé programmation réactive différenciée .

Par exemple, dans un traitement de texte, le marquage des fautes d'orthographe n'a pas besoin d'être totalement synchronisé avec l'insertion de caractères. Ici, la programmation réactive différenciée pourrait potentiellement être utilisée pour donner une priorité inférieure au correcteur orthographique, lui permettant d'être retardé tout en gardant les autres flux de données instantanés.

Cependant, une telle différenciation introduit une complexité de conception supplémentaire. Par exemple, décider comment définir les différentes zones de flux de données et comment gérer le transfert d'événements entre différentes zones de flux de données.

Modèles d'évaluation de la programmation réactive

L'évaluation des programmes réactifs n'est pas nécessairement basée sur la façon dont les langages de programmation basés sur la pile sont évalués. Au lieu de cela, lorsque certaines données sont modifiées, la modification est propagée à toutes les données dérivées partiellement ou complètement des données modifiées. Cette propagation de changement pourrait être réalisée de plusieurs manières, la manière la plus naturelle étant peut-être un schéma d'invalidation/revalidation paresseuse.

Il pourrait être problématique simplement de propager naïvement un changement à l'aide d'une pile, en raison de la complexité de mise à jour exponentielle potentielle si la structure de données a une certaine forme. Une telle forme peut être décrite comme une "forme de losanges répétés", et a la structure suivante : A n →B n →A n+1 , A n →C n →A n+1 , où n=1,2... Ce problème pourrait être surmonté en propageant l'invalidation uniquement lorsque certaines données ne sont pas déjà invalidées, et en revalidant ultérieurement les données si nécessaire à l'aide de l' évaluation paresseuse .

Un problème inhérent à la programmation réactive est que la plupart des calculs qui seraient évalués et oubliés dans un langage de programmation normal doivent être représentés dans la mémoire sous forme de structures de données. Cela pourrait potentiellement rendre la programmation réactive très consommatrice de mémoire. Cependant, des recherches sur ce qu'on appelle l' abaissement pourraient potentiellement surmonter ce problème.

D'un autre côté, la programmation réactive est une forme de ce que l'on pourrait qualifier de « parallélisme explicite », et pourrait donc être bénéfique pour l'utilisation de la puissance du matériel parallèle.

Similitudes avec le modèle d'observateur

La programmation réactive présente des similitudes principales avec le modèle d'observateur couramment utilisé dans la programmation orientée objet . Cependant, l'intégration des concepts de flux de données dans le langage de programmation faciliterait leur expression et pourrait donc augmenter la granularité du graphe de flux de données. Par exemple, le modèle d'observateur décrit généralement des flux de données entre des objets/classes entiers, alors que la programmation réactive orientée objet pourrait cibler les membres d'objets/classes.

Approches

Impératif

Il est possible de fusionner la programmation réactive avec la programmation impérative ordinaire. Dans un tel paradigme, les programmes impératifs fonctionnent sur des structures de données réactives. Un tel montage est analogue à la programmation impérative par contraintes ; Cependant, alors que la programmation impérative par contraintes gère les contraintes bidirectionnelles, la programmation impérative réactive gère les contraintes de flux de données unidirectionnelles.

Orienté objet

La programmation réactive orientée objet (OORP) est une combinaison de programmation orientée objet et de programmation réactive. La manière la plus naturelle de faire une telle combinaison est peut-être la suivante : au lieu de méthodes et de champs, les objets ont des réactions qui se réévaluent automatiquement lorsque les autres réactions dont ils dépendent ont été modifiées.

Si un langage OORP conserve ses méthodes impératives, il entrerait également dans la catégorie de la programmation réactive impérative.

Fonctionnel

La programmation réactive fonctionnelle (FRP) est un paradigme de programmation pour la programmation réactive sur la programmation fonctionnelle .

Basé sur l'acteur

Des acteurs ont été proposés pour concevoir des systèmes réactifs, souvent en combinaison avec la programmation réactive fonctionnelle (FRP) pour développer des systèmes réactifs distribués.

Basé sur des règles

Une catégorie relativement nouvelle de langages de programmation utilise des contraintes (règles) comme concept de programmation principal. Il se compose de réactions aux événements, qui maintiennent toutes les contraintes satisfaites. Non seulement cela facilite les réactions basées sur les événements, mais cela rend les programmes réactifs essentiels à l'exactitude du logiciel. Un exemple de langage de programmation réactif basé sur des règles est l'esperluette, qui est fondé sur l' algèbre relationnelle .

Implémentations

  • ReactiveX , une API pour la mise en œuvre de la programmation réactive avec des flux, des observables et des opérateurs avec des implémentations en plusieurs langages, notamment RxJs, RxJava, .NET, RxPy et RxSwift.
  • Elm (langage de programmation) Composition réactive d'interface utilisateur web.
  • Reactive Streams , une norme JVM pour le traitement de flux asynchrone avec contre-pression non bloquante
  • ObservableComputations , une implémentation .NET multiplateforme.

Voir également

Les références

  1. ^ Treillis, modèle-vue-contrôleur et le modèle d'observateur , communauté Tele.
  2. ^ "Intégration du flux de données dynamique dans un langage d'appel par valeur" . cs.brown.edu . Récupéré le 09/10/2016 .
  3. ^ "Crossing State Lines: Adapter les cadres orientés objet aux langages réactifs fonctionnels" . cs.brown.edu . Récupéré le 09/10/2016 .
  4. ^ "Programmation réactive - L'art du service | Le guide de gestion informatique" . theartofservice.com . Récupéré le 02/07/2016 .
  5. ^ Burchett, Kimberley; Cooper, Gregory H ; Krishnamurthi, Shriram, "L'abaissement: une technique d'optimisation statique pour une réactivité fonctionnelle transparente", Actes du symposium 2007 ACM SIGPLAN sur l'évaluation partielle et la manipulation de programme basée sur la sémantique (PDF) , pp. 71-80.
  6. ^ Demetrescu, Camil; Finocchi, Irène ; Ribichini, Andrea (22 octobre 2011), "Reactive Imperative Programming with Dataflow Constraints", Actes de la conférence internationale ACM 2011 sur les langages et applications de systèmes de programmation orientés objet , Oopsla '11, pp. 407–26, doi : 10.1145/2048066.2048100 , ISBN 9781450309400, S2CID  7285961.
  7. ^ Van den Vonder, Sam; Renaux, Thierry ; Oeyen, Bjarno ; De Koster, Joeri; De Meuter, Wolfgang (2020), "Tackling the Awkward Squad for Reactive Programming: The Actor-Reactor Model", Leibniz International Proceedings in Informatics (LIPIcs) , 166 , pp. 19:1–19:29, doi : 10.4230/LIPIcs .ECOOP.2020.19 , ISBN 9783959771542.
  8. ^ Shibanai, Kazuhiro; Watanabe, Takuo (2018), "Distributed Functional Reactive Programming on Actor-Based Runtime", Actes du 8e ACM SIGPLAN International Workshop on Programming Based on Actors, Agents, and Decentralized Control , Agere 2018, pp. 13-22, doi : 10.1145/3281366.3281370 , ISBN 9781450360661, S2CID  53113447.
  9. ^ Joosten, Stef (2018), "Relation Algebra comme langage de programmation utilisant le compilateur Esperluette", Journal of Logical and Algebraic Methods in Programming , 100 , pp. 113-29, doi : 10.1016/j.jlamp.2018.04.002.

Liens externes