Reactive programming 101

David Palita
WhozApp
Published in
9 min readApr 4, 2018

La programmation réactive est à la mode en ce moment. Elle est notamment au coeur d’angular et de spring 5. Nous allons voir quels en sont les principes, à quoi ça ressemble et quels sont les avantages et inconvénients par rapport à d’autres styles de programmation.

Tiré du reactive manifesto (ça fait toujours bien d’avoir un manifest) :

… we want systems that are Responsive, Resilient, Elastic and Message Driven. We call these Reactive Systems.

et de wikipedia :

In computing, reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change.

Imperative, declarative, event driven, object oriented, functional, reactive…

L’immense majorité de la programmation est impérative, c’est à dire qu’on écrit une suite d’instruction à exécuter pour réaliser une tâche.
Il existe de nombreux styles de programmation impérative qui ne sont pas exclusifs.

Il y a par exemple le style procédural qui peut être vu comme une recette de cuisine complètement linéaire. C’est facile à lire, à comprendre et à débugger. Le problème c’est que ça ne passe pas facilement à l’échelle supérieure, et que ça devient vite inmaintenable.
L’orienté objet est une façon d’organiser la programmation impérative avec quelques tabous (goto c’est hyper mal), une encapsulation des accès aux données et la définition de périmètre de responsabilité.
Le style fonctionnel est une autre façon d’organiser, avec une emphase sur l’immutabilité et l’absence d’effets de bords.
Si on revient à la recette on peut maintenant utiliser la fonction “faire une mayonnaise” plutôt que la procédure “casser 2 oeufs, les battre avec de l’huile et du vinaigre, etc”. On peut aussi utiliser l’objet “robot” avec sa méthode “battre” plutôt que “appliquer un courant de 12V continu au moteur électrique, embrayer telle sortie pour appliquer une rotation de 200 tours/minute au batteur, etc”. On voit que chaque partie du programme se spécialise et on n’a plus besoin de tout maintenir d’un bloc.
Event driven introduit un facteur externe : le flux d’exécution ne sera pas déterministe car essentiellement dépendant d’événements externes.

La programmation déclarative c’est très différent : on cherche à exprimer ce que doit faire le programme plutôt que comment. C’est le graal des langages de 4ème génération. Je vois assez peu de véritables exemples, SQL et CSS en sont. Pa exemple, on ne dit pas à la base de lire tel index avant d’utiliser un cache pour récupérer le bloc de données à joindre avec… etc, on dit simplement ce qu’on veut et la base se charge de répondre à la demande.

On va s’intéresser ici à reactive qu’on peut voir comme un moyen de rendre plus déclarative une programmation fonctionnelle et event driven.

La programmation reactive résout la question de la propagation du changement dans un programme dépendant de “reactive values”, c’est à dire d’inputs dont les valeurs évoluent dans le temps. On parle aussi de “stream of values”. Par exemple la position de la souris sur un écran, le cours du bitcoin, le contenu d’une base de données, …

C’est une forme d’inversion de contrôle, ce qui induit un changement de paradigme comme pour l’injection de dépendance. DI inverse le contrôle de l’instanciation dans une application : c’est le framework qui instancie et assemble les objets dépendants. Reactive inverse le contrôle du flux d’exécution : c’est le framework qui enchaîne les appels dans le bon ordre et au bon moment.

Avant (mal) : Value

Au commencement était la fonction retournant une valeur (oublions dès à présent les infidèles qui utilisent des fonctions à effet de bord) :

Tout simple : une invocation avec des paramètres => une valeur retournée (et les moutons étaient bien gardés).

Mais rapidement on s’est retrouvé avec des traitements longs, asynchrones, parallèles, de sources et d’impacts variés. La fonction à retour de valeur était bien embêtée parce que soit elle devait bloquer son thread en attendant le retour, soit elle devait prendre en paramètre supplémentaire un callback à appeler comme callback du callback de l’opération asynchrone. Si ce callback était également asynchrone il devait aussi prendre en paramètre un callback etc. Et quand 2 callbacks étaient interdépendants ou qu’un troisième les attendait il leur restait leurs yeux pour pleurer. #callbackhell

Après (mieux) : promise

Le deuxième jour Friedman (et al) inventa la Promise (en 1976 mais on s’est massivement dit que c’était une bonne idée dans les années 2000). Une promise (ou future, ou delay, ou deferred) c’est un proxy pour la valeur, en attendant qu’elle arrive. Il permet à notre fonction d’avoir un truc à retourner tout de suite, que l’appelant pourra utiliser (ou le passer à quelqu’un d’autre) à sa guise s’il veut faire quelque chose quand (et si ) il y aura un résultat.
Un exemple de promesse est la reconnaissance de dette : ce n’est pas encore de l’argent, mais ça doit permettre de l’obtenir (ou de sortir en erreur avec un défaut de paiement) voire de la transmettre car elle est réputée avoir une valeur si le débiteur est solvable.

Moins simple : une invocation avec des paramètres, un proxy retourné pour lire les résultats plus tard par le biais de fonctions de traitement.

Beaucoup plus simple quand même que la valeur parce qu’on passe à une écriture plus déclarative, qu’on n’imbrique pas les callbacks de manière aussi agressive (ou pire qu’on fasse un polling pour avoir le résultat) et qu’on a des fonctions de plus haut niveau pour combiner les promises.

C’est bien mais c’est tout. Une promise peut être pending (pas de valeur) resolved (une valeur) ou rejected (une erreur). Une fois utilisée elle est à jeter, elle ne produira plus de nouvelles valeurs.

Maintenant (bien) : observable

Enfin le GoF catalogua les observables (en 1994, mais on a attendu les années 2010 pour une adoption massive, ce qui est déjà deux fois plus vite que pour les promise, le prochain pattern sera-t-il généralisé sous cinq ans ?).

On peut voir les observables comme une généralisation des promises. Ou comme des fonctions qui peuvent retourner plusieurs fois. Comme dans tout bon western, on a trois protagonistes : l’observable, le stream et l’observer.

L’observer c’est celui qui traite la sortie, la fonction utilisée dans le then de la promise, la suite de la procédure en version impérative.
Le stream c’est le flux de valeurs qui traversent l’observable pour stimuler l’observer. On peut le voir comme un tableau, mais dans le temps. On verra que de nombreuses fonctions de l’API Array ont leurs cousines dans l’API Rx.
L’observable c’est le plan de transformation des valeurs du stream pour aboutir à un résultat exploitable par l’observer. En général un observable est constitué d’une succession d’observers eux-même observables d’une valeur modifiée du stream. On parle de chaîne ou de pipeline d’observable, cela corre spond à la définition d’une fonction en programmation purement impérative : ce qu’il faudra faire quand on l’appellera.

Certains l’aiment chaud

Une différence fondamentale entre les versions observable et array ci-dessus c’est que avant le forEach on a [1, 9, 25], alors qu’avant le subscribe on a … un observable. Pas de valeur, les fonctions passées à filter et map n’ont pas été appelées.

Un observable c’est un plan d’exécution, c’est complètement déclaratif, comme la déclaration d’une fonction. Tant qu’il n’a pas d’observer, il ne se passe rien (ce qui peut valoir quelques heures de souffrance et de désespoir au début).

Quand et tant que l’observable a un observer le stream passe à travers, les fonctions sont exécutées et l’observer est sollicité.

Chacun sa route

Un autre aspect important c’est que ce plan d’exécution va être mis en oeuvre de manière indépendante pour chaque observer (à moins de mettre explicitement un observer multicast dans la chaîne).

On peut alors avoir 2 cas :
- la source de données (aka producer) est créée et déclenchée dans l’observable, celui-ci sera alors dit “cold” et chaque observer aura sa propre source. Exemple : une vidéo youtube (on ne veut pas la continuer là ou le dernier utilisateur en est)
- la source de données est créée hors de l’observable et déclenchée dans l’observable, celui-ci est alors dit “hot” et chaque observer verra la même chose en même temps. Exemple : un live facebook (le présentateur ne veut pas recommencer à chaque fois que quelqu’un se connecte)

Ce qui fait qu’un opérateur multicast transforme en général un observable “cold” en observable “hot”.

Encore une petite chose, ou pas

Un observable a deux autres événements optionnels (et exclusifs) : complete et error. Une requête http est un exemple d’observable qui complete, un observable des messages d’un websocket est un exemple d’observable qui ne complete pas forcément. Les deux peuvent sortir en error.

Une fois l’un ou l’autre advenu, il n’y aura plus de nouvelles valeurs, les souscriptions sont terminées. À l’inverse tant que l’observable n’est pas “completed” les souscriptions sont actives. C’est une excellente source de fuite mémoire, si besoin.

Celui qui a tout suivi voit donc qu’un observable multicast qui complète après une unique émission de valeur se comporte comme une fonction qui retourne une promise, dès lors qu’il a eu un premier observer.

Rx

Il est très difficile d’implémenter correctement le pattern Observable. On ne part donc pas en mode b*** et couteau, on utilise une librairie réactive (il y a des languages réactifs mais ils me semblent anecdotiques aujourd’hui).

Notre librairie de prédilection c’est Rx (pour Reactive eXtensions), à l’origine un truc de MS avec Rx.NET, fortement développé par netflix avec RxJava et base de Angular avec rxjs. Il en existe évidemment d’autres (akka et reactor par ex).

Rx c’est une implémentation d’un certain nombre d’artefacts (Observer, Observable, Subject, Scheduler, etc) mais surtout d’une collection d’opérateurs de manipulation du stream. Beaucoup sont similaires à ceux de l’API Array (filter, map, reduce, …), d’autres sont plus spécifiquement dédiés à la gestion du temps et de la concurrence (debounce, window, forkJoin, …)

Par nature les opérateurs fonctionnent dans le temps, et c’est souvent assez difficile d’avoir une représentation de ce qu’ils font. Pour commencer les marble diagrams sont une excellente ressource.

C’est bien beau tout ça, mais à quoi ça sert ?

La beauté intrinsèque de cette façon de réfléchir se suffit bien sûr à elle-même, mais il y a aussi de nombreuses applications concrètes.

L’intérêt principal vient de ce que la programmation reactive permet de maintenir la cohérence et de contenir la complexité d’un programme massivement asynchrone (aka parallèle, aka concurrent).

Optimisation des ressources

Le fait d’inverser le contrôle d’exécution permet une bien meilleure utilisation des ressources.

On passe d’un mode pull à un mode push. Par exemple, imaginons qu’on veuille l’avis d’un quidam sur un livre. Cela revient à passer de :
“Lis ce bouquin et dis moi ce que tu en penses. — croise les bras et tappe du pied en attendant — écoute le commentaire du lecteur“
à :
“Lis ce bouquin et appelle moi pour me dire ce que tu en penses — va jouer à la console en attendant, répond au téléphone pour écouter le commentaire du lecteur”.

Perso, je préfère jouer à la console, et ça s’applique très bien à tous les IO d’un programme.

NB : une autre possibilité aurait été — joue à la console et pause toutes les 10 minutes pour appeler le lecteur et lui demander s’il a fini le livre — mais ça a tendance à vriller les nerfs du quidam.

Enrichissement de comportement

Le fait de passer dans un style de programmation plus déclaratif rend plus simple l’ajout de nouvelles fonctionnalités basées sur le même reactive stream.

Au lieu de faire une procédure complète on peut facilement créer des chaînes d’observables dérivées.

Backpressure

Sujet trop compliqué pour ce post, mais en gros l’idée est que comme producteurs et consommateurs se connaissent par le biais de la chaîne d’observables, ils peuvent négocier le débit du flux et éviter d’écrouler un consommateur sous une montagne de sollicitations ou exploser la mémoire d’un producteur qui n’arrive pas à se débarrasser de ses données.

Cf reactive streams et une explication de méthodes de gestion de pression.

Plus que tout, ou rien

Pour tirer complètement partie de la philosophie réactive, il faut que la totalité de l’application soit réactive. Il est difficile de mélanger réactif et non réactif, et cela conduit très facilement à des bugs.

En revanche quand toute l’application est réactive on a l’assurance de sa cohérence et de sa consistance. Et chocolat sur la cerise sur le gâteau : son modèle de concurrence n’a (presque) plus d’importance. Que les opérations soient synchrones ou non n’impacte plus le résultat.

Une note de fin : comme pour DI, reactive rend beaucoup plus simple des choses très compliquées, mais rend aussi bien plus compliquées des choses très simple. Un hello world ne permet pas de se rendre compte de l’intérêt, il faut pratiquer sur un cas réel d’application pour accéder à l’illumination.

Let’s dance

Pour mettre tout ça en pratique j’ai implémenté un jeu de Simon entièrement à base d’observables. Ce jeu met en oeuvre un certain nombre d’opérateurs et de techniques en situation réelle. La source est très commentée et vous pouvez tout modifier sur stackblitz pour vous approprier les comportements des opérateurs rxjs :

reactive simon says

--

--