Pratiques fonctionnelles et frontend: monades et foncteurs

Bonjour à tous! Je m'appelle Dmitry Rudnev, je suis développeur front-end chez BCS. J'ai commencé mon voyage avec la disposition d'interfaces de complexité variable et j'ai toujours prêté une attention particulière à l'interface: à quel point il serait confortable pour l'utilisateur d'interagir avec lui, si je pouvais transmettre à l'utilisateur l'interface même telle que le concepteur l'a conçue.



Dans cette série d'articles, je veux partager mon expérience de l'application de pratiques fonctionnelles dans le développement frontend, je parlerai des avantages et des inconvénients que vous recevrez en tant que développeur utilisant ces pratiques. Si vous aimez le thème, alors nous plongerons dans les coins «plus secs» et plus complexes du monde fonctionnel. Je note tout de suite que nous passerons du plus grand au plus petit, c'est-à-dire que nous examinerons l'application classique à vol d'oiseau, et que nous parcourrons les articles, nous descendrons là où une pratique spécifique nous apportera des avantages notables.

Commençons donc par gérer les états. En même temps, je vais vous dire, et ici, en général, des monades et des foncteurs.

Intro


En découvrant la prochaine interface et en trouvant un terrain d'entente entre l'interface utilisateur et l'analyse, j'ai commencé à remarquer que chaque fois qu'un développeur traite avec un réseau, il a juste besoin de traiter tous les états de l'interface utilisateur et de décrire la réaction à un état particulier. Et comme chacun de nous aspire à l'excellence, il y a un désir pour cette façon de traiter les états de faire ressortir un schéma qui décrit le plus transparent possible ce qui se passe et ce qui est l'initiateur d'une réaction particulière, et par conséquent, le résultat du travail. Heureusement, dans le monde de la programmation, presque tout ce à quoi vous pouvez penser a été implémenté par quelqu'un avant vous.

À la fois dans le monde du développement et dans le monde du design, non seulement des modèles ont été formés qui vous permettent de résoudre efficacement vos problèmes, mais aussi des antipatters, qui devraient être évités par tous les moyens afin que les mauvaises pratiques ne se développent pas, et le développeur ou le concepteur a toujours pris pied dans les situations, quand il n'y a pas de solution concrète.

Dans notre cas, la situation de la plupart des développeurs est le traitement de tous les états de l'élément d'interface utilisateur et leur réaction. Le problème ici est que l'élément d'interface utilisateur peut interagir avec l'état local (sans exécuter de requêtes asynchrones) et avec des ressources ou des référentiels distants. Les développeurs oublient parfois de gérer tous les cas marginaux, ce qui entraîne un comportement incohérent du système dans son ensemble.

Tous les exemples contiendront des exemples de code utilisant la bibliothèque React et un sur-ensemble de JavaScript - TypeScript, ainsi que des bibliothèques pour la programmation fonctionnelle fp-ts.

Prenons l'exemple le plus simple, où nous avons une liste d'éléments que nous demandons au serveur et où nous devons afficher correctement l'interface utilisateur conformément au résultat de la demande. Nous nous intéressons à la fonction de render , car nous devons y afficher l'état correct lors de l'exécution de la demande. L'exemple de code complet peut être consulté sur: application simple . À l'avenir, un projet complet sera disponible, axé sur une série d'articles, où nous démonterons au cours de ses différentes parties.

  const renderInitial = (...) => ...; const renderPending = (...) => ...; const renderError = (...) => ... ; const renderSuccess = (...) => ... ; return ( {state.subcribers.foldL( renderInitial, renderPending, renderError, renderSuccess, )} ); 

L'exemple montre clairement que chaque état du modèle de données a sa propre fonction, et chaque fonction renvoie un fragment de l'interface utilisateur qui lui a été prescrite (message, bouton, etc.). Pour l'avenir, je dirai que l'exemple utilise la RemoteData monad .

C'est tellement élégant et surtout sûr que nous pouvons travailler avec des données et y répondre. C'était l'introduction, où j'ai essayé de démontrer les avantages d'une approche fonctionnelle dans un exemple aussi simple.

Functor et Monad


Maintenant, commençons à nous plonger progressivement dans la théorie appliquée des catégories et analysons des concepts tels que Functor et Monad , et considérons également des pratiques pour travailler avec des données en toute sécurité en utilisant des pratiques fonctionnelles.

«Essentiellement, un foncteur n'est rien de plus qu'une structure de données qui vous permet d'appliquer des fonctions de transformation pour extraire des valeurs d'un shell, les modifier, puis les replacer dans le shell.

Enfermer des valeurs dans un shell ou un conteneur est un modèle de conception fondamental dans la programmation fonctionnelle, car il protège contre l'accès direct aux valeurs, leur permettant d'être manipulées en toute sécurité et sans changement dans les programmes d'application.

J'ai pris cette citation d'un merveilleux livre sur la revue des techniques de programmation fonctionnelle en JavaScript . Commençons par la composante théorique et analysons ce qu'est réellement un foncteur. Pour commencer, nous devons nous familiariser avec une section fascinante des mathématiques appelée théorie des catégories au niveau le plus élémentaire.

La théorie des catégories est une branche des mathématiques qui étudie les propriétés des relations entre les objets mathématiques, indépendamment de la structure interne des objets. La théorie des catégories occupe une place centrale dans les mathématiques modernes; elle a également trouvé des applications en informatique, en logique et en physique théorique.

Une catégorie se compose d'objets et de flèches dirigés entre eux. La façon la plus simple de visualiser une catégorie est:

Les flèches sont disposées de sorte que si vous avez une flèche de l'objet A à l'objet B et une flèche de l'objet B à C , alors il doit y avoir une flèche - leur composition est de A à C. Considérez les flèches comme des fonctions; ils sont aussi appelés morphismes. Vous avez une fonction f qui prend A comme argument et renvoie B. Il existe une autre fonction g qui prend B comme argument et retourne C. Vous pouvez les combiner en passant le résultat de f à g . Nous venons de décrire une nouvelle fonction qui prend A et retourne C. En mathématiques, une telle composition est désignée par un petit cercle entre la notation de fonction: g ◦ f. Faites attention à l'ordre de composition - de droite à gauche.

En mathématiques, la composition est dirigée de droite à gauche. Dans ce cas, cela aide si vous lisez g ◦ f comme «g après f».

 -—   A  B f :: A -> B -—   B   g :: B -> C -— A  C g . f 

Il y a deux propriétés très importantes qu'une composition doit satisfaire dans n'importe quelle catégorie.

  1. La composition est associative (l'associativité est une propriété des opérations qui vous permet de restaurer la séquence de leur exécution en l'absence d'indications explicites de succession avec une priorité égale; cela distingue entre l'associativité gauche, où l'expression est évaluée de gauche à droite, et l'associativité droite de droite à gauche. Les opérateurs correspondants sont appelés associatifs à gauche et associatifs à droite. Si vous avez trois morphismes (flèches), f, g et h, qui peuvent être arrangés (c'est-à-dire que leurs types sont cohérents les uns avec les autres), vous les besoins entre parenthèses à grouper. Mathématiquement, cela est écrit en tant que h ◦ (g ◦ f) = (h ◦ g) ◦ f = h ◦ g ◦ f (h ◦ g) ◦ f = h ◦ g ◦ f
  2. Pour chaque objet A, il y a une flèche, qui sera une unité de composition. Cette flèche va d'un objet à lui-même. Être une unité de composition signifie que lors de la composition d'une unité avec n'importe quelle flèche qui commence sur A ou se termine sur A, respectivement, la composition renvoie la même flèche. La flèche unitaire d'un objet A est appelée IDa (unité sur A). En notation mathématique, si f passe de A à B, alors f ◦ idA = f

    Pour travailler avec des fonctions, une seule flèche est implémentée comme une fonction identique, qui renvoie simplement son argument.

Nous pouvons maintenant considérer ce qu'est un foncteur dans la théorie des catégories.

Un foncteur est un type spécial de mappage entre les catégories. Il peut être compris comme un affichage qui préserve la structure. Les foncteurs entre les petites catégories sont des morphismes dans la catégorie des petites catégories. La totalité de toutes les catégories n'est pas une catégorie au sens habituel, puisque la totalité de ses objets n'est pas une classe. - Wikipédia .

Prenons un exemple d'implémentation d'un foncteur pour le conteneur Maybe, qui est l'idée d'une «valeur qui peut être absente».

 const compose = <A, B, C>( f: (a: A) => B, g: (b: B) => C, ): (a: A) => C => (a: A) => g(f(a)); //  Maybe: type Nothing = Readonly<{ tag: 'Nothing' }>; type Just<A> = Readonly<{ tag: 'Just'; value: A }>; export type Maybe<A> = Nothing | Just<A>; const nothing: Nothing = { tag: 'Nothing' }; const just = <A>(value: A): Just<A> => ({ tag: 'Just', value }); //    Maybe: const fmap = <A, B>(f: (a: A) => B) => (fa: Maybe<A>): Maybe<B> => { switch (fa.tag) { case 'Nothing': return nothing; case 'Just': return just(f(fa.value)); } }; //  1: fmap id === id namespace Laws { console.log( fmap(id)(just(42)), id(just(42)), ); // => { tag: 'Just', value: 42 } //  2: fmap f ◦ fmap g === fmap (f ◦ g) const f = (a: number): string => `Got ${a}!`; const g = (s: string): number => s.length; console.log( compose(fmap(f), fmap(g))(just(42)), fmap(compose(f, g))(just(42)), ); // => { tag: 'Just', value: 7 } } 

La méthode fmap peut être vue de deux côtés:

  1. Comme moyen d'appliquer une fonction pure à une valeur «conteneurisée»;
  2. Pour «élever dans le contexte du conteneur» une fonction pure.

En effet, si les crochets de l'interface sont légèrement différents, on peut obtenir la signature de la fonction fmap :

 const fmap: <A, B>(f: (a: A) => B) => ((ma: Maybe<A>) => Maybe<B>); 

Après avoir défini l'interface:

 type Function1<Domain, Codomain> = (a: Domain) => Codomain; 

on obtient la définition de fmap :

 const fmap: <A, B>(f: (a: A) => B) => Function1<Maybe<A>, Maybe<B>>; 

Cette astuce simple nous permet de penser à un foncteur comme un moyen «d'élever une fonction pure dans un contexte de conteneur». Grâce à cela, il est possible de travailler avec différents types de données de manière sûre: par exemple, traiter avec succès des chaînes de valeurs imbriquées facultatives; Convertir des listes de données gérer les exceptions et plus encore.

Comme expliqué précédemment, à l'aide de foncteurs, vous pouvez appliquer des fonctions à des valeurs en toute sécurité et de manière inchangeable. Les monades sont similaires aux foncteurs, sauf qu'elles sont capables de déléguer une logique spéciale dans des cas particuliers. Le foncteur lui-même ne sait que comment appliquer cette fonction et envelopper le résultat dans un shell, et il n'a pas de logique supplémentaire.

Une monade apparaît lors de la création d'un type de données complet par le principe d'extraction de données par le principe d'extraction de valeurs à partir de shells et de définition de règles à partir de l'imbrication. Comme les foncteurs, les monades sont un modèle de conception utilisé pour décrire des calculs sous la forme d'une séquence d'étapes où la valeur traitée n'est pas connue du tout, mais ce sont les monades qui permettent de contrôler en toute sécurité et sans effets secondaires le flux de données lorsqu'elles sont utilisées dans la composition. Les monades peuvent viser à résoudre une variété de problèmes. Théoriquement, les monades dépendent du système de types dans une langue particulière. En fait, beaucoup de gens pensent qu'ils ne peuvent être compris que s'il existe des types de données explicites.

Afin de mieux comprendre les monades, les concepts importants suivants doivent être appris.
Monade Fournit une interface abstraite pour les opérations monadiques
Type monadique. Implémentation spécifique de cette interface

Mais des exemples pratiques de l'application de ces propriétés d'un foncteur et d'autres constructions catégoriques que je montrerai dans de futurs articles.

Source: https://habr.com/ru/post/fr471170/


All Articles