Gérer les effets secondaires sales dans du code JavaScript pur et fonctionnel

Si vous vous essayez à la programmation fonctionnelle, cela signifie que vous rencontrerez bientôt le concept de fonctions pures. Au fur et à mesure que vous continuez, vous constaterez que les programmeurs qui préfèrent un style fonctionnel semblent être obsédés par ces fonctionnalités. Ils disent que les fonctions pures vous permettent de parler de code. Ils disent que les fonctions pures sont des entités qui ne fonctionneront probablement pas si imprévisiblement qu'elles mèneront à une guerre thermonucléaire. Vous pouvez également apprendre de ces programmeurs que les fonctions pures offrent une transparence référentielle. Et ainsi - à l'infini.

Soit dit en passant, les programmeurs fonctionnels ont raison. Les fonctions pures sont bonnes. Mais il y a un problème ...


L'auteur du document, dont nous vous présentons la traduction, souhaite parler de la manière de gérer les effets secondaires dans les fonctions pures.

Le problème des fonctions pures


Une fonction pure est une fonction qui n'a pas d'effets secondaires (en fait, ce n'est pas une définition complète d'une fonction pure, mais nous reviendrons sur cette définition). Cependant, si vous comprenez au moins quelque chose en programmation, alors vous savez que la chose la plus importante ici est précisément les effets secondaires. Pourquoi calculer le nombre Pi jusqu'à la centième décimale si personne ne peut lire ce nombre? Afin d'afficher quelque chose à l'écran ou d'imprimer sur une imprimante, ou de le présenter sous une autre forme, accessible à la perception, nous devons appeler la commande appropriée à partir du programme. Et à quoi servent les bases de données si rien ne peut y être écrit? Pour garantir le fonctionnement des applications, vous devez lire les données des périphériques d'entrée et demander des informations aux ressources réseau. Tout cela ne peut se faire sans effets secondaires. Mais malgré cet état de fait, la programmation fonctionnelle est construite autour de fonctions pures. Alors, comment les programmeurs qui écrivent des programmes dans un style fonctionnel parviennent-ils à résoudre ce paradoxe?

Si vous répondez à cette question en un mot, les programmeurs fonctionnels font la même chose que les mathématiciens: ils trichent. Bien que, malgré cette accusation, il faut dire qu'ils, d'un point de vue technique, suivent simplement certaines règles. Mais ils trouvent des failles dans ces règles et les étendent à des tailles incroyables. Ils le font de deux manières principales:

  1. Ils profitent de l'injection de dépendance. J'appelle cela jeter un problème par-dessus une clôture.
  2. Ils utilisent des foncteurs, ce qui me semble une forme extrême de procrastination. Il convient de noter ici que dans Haskell, il est appelé «IO functor» ou «IO monad », dans PureScript, le terme «Effect» est utilisé, ce qui, à mon avis , est un peu mieux pour décrire l'essence des foncteurs.

Injection de dépendance


L'injection de dépendance est la première façon de gérer les effets secondaires. En utilisant cette approche, nous prenons tout ce qui pollue le code et le mettons dans les paramètres de la fonction. Ensuite, nous pouvons considérer tout cela comme faisant partie de la responsabilité d'une autre fonction. Je vais l'expliquer avec l'exemple suivant:

// logSomething :: String -> String function logSomething(something) {    const dt = (new Date())toISOString();    console.log(`${dt}: ${something}`);    return something; } 

Ici, je voudrais faire une note pour ceux qui connaissent les signatures de type. Si nous respections strictement les règles, nous devions alors prendre en compte les effets secondaires. Mais nous y reviendrons plus tard.

La fonction logSomething() a deux problèmes qui l'empêchent d'être déclarée propre: elle crée un objet Date et affiche quelque chose dans la console. Autrement dit, notre fonction effectue non seulement des opérations d'entrée-sortie, mais elle produit également, lorsqu'elle est appelée à des moments différents, des résultats différents.

Comment rendre cette fonction propre? En utilisant la technique d'injection de dépendances, nous pouvons prendre tout ce qui pollue la fonction et en faire des paramètres de fonction. Par conséquent, au lieu d'accepter un paramètre, notre fonction acceptera trois paramètres:

 // logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) {   const dt = d.toIsoString();   return cnsl.log(`${dt}: ${something}`); } 

Maintenant, pour appeler la fonction, nous devons lui transférer tout ce qui l'a polluée auparavant:

 const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something); //  "Curiouser and curiouser!" 

Ici, vous pouvez penser que tout cela est absurde, que nous n'avons déplacé le problème que d'un niveau vers le haut, et cela n'a pas ajouté de la pureté à notre code. Et vous savez, ce sont les bonnes pensées. Il s'agit d'une faille dans sa forme la plus pure.

Cela ressemble à un analphabétisme factice: «Je ne savais pas que l'appel de la méthode log de l'objet cnsl conduirait à l'exécution de l'instruction d'E / S. Quelqu'un vient de me le remettre, mais je ne sais pas d'où tout cela vient. " Cette attitude est fausse.

Et, en fait, ce qui se passe n'est pas aussi stupide que cela puisse paraître à première vue. logSomething() œil aux fonctionnalités de la fonction logSomething() . Si vous voulez faire quelque chose d'impur, vous devez le faire vous-même. Disons que vous pouvez passer différents paramètres à cette fonction:

 const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = {   log: () => {       //      }, }; logSomething(d, cnsl, "Off with their heads!"); //   "Off with their heads!" 

Maintenant, notre fonction ne fait rien (elle ne renvoie que le paramètre something ). Mais elle est complètement pure. Si vous l'appelez plusieurs fois avec les mêmes paramètres, il retournera la même chose à chaque fois. Et c'est tout. Afin de rendre cette fonction impure, nous devons effectuer intentionnellement certaines actions. Ou, pour le dire autrement, tout ce dont dépend une fonction est dans sa signature. Il n'accède à aucun objet global comme la console ou la Date . Cela formalise tout.

De plus, il est important de noter que nous pouvons transférer d'autres fonctions à notre fonction, qui n'était pas auparavant propre. Jetez un oeil à un autre exemple. Imaginez que dans une certaine forme il y ait un nom d'utilisateur et nous devons obtenir la valeur du champ correspondant de cette forme:

 // getUserNameFromDOM :: () -> String function getUserNameFromDOM() {   return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; //   "mhatter" 

Dans ce cas, nous essayons de charger certaines informations à partir du DOM. Les fonctions pures ne le font pas, car le document est un objet global qui peut changer à tout moment. Une façon de rendre une telle fonction propre est de lui passer l'objet document global en tant que paramètre. Cependant, vous pouvez toujours lui passer la fonction querySelector() . Cela ressemble à ceci:

 // getUserNameFromDOM :: (String -> Element) -> String function getUserNameFromDOM($) {   return $('#username').value; } // qs :: String -> Element const qs = document.querySelector.bind(document); const username = getUserNameFromDOM(qs); username; //   "mhatter" 

Ici, encore une fois, vous pouvez penser que c'est stupide. Après tout, ici, nous avons simplement supprimé de la fonction getUsernameFromDOM() ce qui ne nous permet pas de l'appeler propre. Cependant, nous ne nous en sommes pas débarrassés, transférant simplement l'appel au DOM vers une autre fonction, qs() . Il pourrait sembler que le seul résultat notable de cette étape était que le nouveau code était plus long que l'ancien. Au lieu d'une fonction impure, nous avons maintenant deux fonctions, dont l'une est toujours impure.

Attends un peu. Imaginez que nous devons écrire un test pour la fonction getUserNameFromDOM() . Maintenant, en comparant les deux options pour cette fonction, pensez à laquelle sera la plus facile à utiliser? Pour que la version sale de la fonction fonctionne, nous avons besoin d'un objet document global. De plus, ce document devrait avoir un élément avec l' username du username . Si vous devez tester une fonction similaire en dehors du navigateur, vous devrez utiliser quelque chose comme JSDOM ou un navigateur sans interface utilisateur. Veuillez noter que tout cela n'est nécessaire que pour tester une petite fonction avec une longueur de plusieurs lignes. Et pour tester la deuxième version propre de cette fonction, il suffit de faire ce qui suit:

 const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`); 

Bien sûr, cela ne signifie pas que pour tester de telles fonctions, des tests d'intégration effectués dans un navigateur réel (ou, du moins, en utilisant quelque chose comme JSDOM) ne sont pas nécessaires. Mais cet exemple montre une chose très importante, c'est que maintenant la fonction getUserNameFromDOM() devenue complètement prévisible. Si nous lui passons qsStub() , il retournera toujours mhatter . L '"imprévisibilité" nous sommes passés à la petite fonction qs() .

Si nécessaire, nous pouvons amener des mécanismes imprévisibles à des niveaux encore plus éloignés de la fonction principale. Par conséquent, nous pouvons les déplacer, relativement parlant, dans les «zones frontalières» du code. Cela nous amènera à avoir une mince couche de code impur qui entoure un noyau bien testé et prévisible. La prévisibilité du code s'avère extrêmement précieuse lorsque la taille des projets créés par les programmeurs augmente.

▍ Inconvénients du mécanisme d'injection de dépendance


En utilisant l'injection de dépendances, vous pouvez écrire une application volumineuse et complexe. Je le sais, puisque j'ai moi-même écrit une telle application . Avec cette approche, les tests sont simplifiés et les dépendances de fonction deviennent clairement visibles. Mais l'injection de dépendance n'est pas sans défauts. Le principal est que lorsqu'il est utilisé, des signatures de fonction très longues peuvent être obtenues:

 function app(doc, con, ftch, store, config, ga, d, random) {   //     } app(document, console, fetch, store, config, ga, (new Date()), Math.random); 

En fait, ce n'est pas si mal. Les inconvénients de telles constructions se manifestent si certains paramètres doivent être passés à certaines fonctions qui sont très profondément intégrées dans d'autres fonctions. Cela ressemble à la nécessité de passer des paramètres à travers de nombreux niveaux d'appels de fonction. Lorsque le nombre de ces niveaux augmente, cela commence à ennuyer. Par exemple, il peut être nécessaire de transférer l'objet représentant la date via 5 fonctions intermédiaires, alors qu'aucune des fonctions intermédiaires n'utilise cet objet. Bien sûr, on ne peut pas dire qu'une telle situation ressemble à une catastrophe universelle. De plus, cela permet de voir clairement les dépendances des fonctions. Quoi qu'il en soit, ce n'est toujours pas si agréable. Par conséquent, nous considérons le mécanisme suivant.

â–Ť Fonctions paresseuses


Jetons un coup d'œil à la deuxième faille utilisée par les adeptes de la programmation fonctionnelle. Il consiste dans l'idée suivante: un effet secondaire n'est pas un effet secondaire jusqu'à ce qu'il se produise réellement. Je sais que cela semble mystérieux. Afin de comprendre cela, considérons l'exemple suivant:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

Un exemple est peut-être stupide, je le sais. Si nous avons besoin du numéro 0, alors pour qu'il apparaisse, il suffit de le saisir au bon endroit dans le code. Et je sais aussi que vous n'écrirez pas de code JavaScript pour contrôler les armes nucléaires. Mais nous avons besoin de ce code pour illustrer la technologie en question.

Voici donc un exemple de fonction impure. Il fournit des données à la console et est également la cause d'une guerre nucléaire. Cependant, imaginez que nous avons besoin du zéro que cette fonction renvoie. Imaginez un scénario dans lequel nous devons calculer quelque chose après le lancement d'une fusée. Disons que nous pourrions avoir besoin de démarrer un compte à rebours ou quelque chose comme ça. Dans ce cas, il serait tout à fait naturel de penser à l'avance aux calculs. Et nous devons nous assurer que la fusée se lance exactement en cas de besoin. Nous n'avons pas besoin d'effectuer des calculs de telle manière qu'ils pourraient conduire accidentellement au lancement de cette fusée. Pensons donc à ce qui se passe si nous fZero() fonction fZero() dans une autre fonction qui la renvoie simplement. Disons que ce sera quelque chose comme un wrapper de sécurité:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   return fZero; } 

Vous pouvez appeler la fonction returnZeroFunc() autant de fois que vous le souhaitez. Dans ce cas, jusqu'à ce que la mise en œuvre de ce qu'il retourne soit effectuée, nous sommes (théoriquement) en sécurité. Dans notre cas, cela signifie que l'exécution du code suivant ne conduira pas à une guerre nucléaire:

 const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc(); //     . 

Maintenant un peu plus strict qu'avant, approchons la définition du terme «fonction pure». Cela nous permettra d'examiner la fonction returnZeroFunc() plus en détail. Ainsi, la fonction est propre dans les conditions suivantes:

  • Aucun effet secondaire observĂ©.
  • Lien de transparence. C'est-Ă -dire que l'appel d'une telle fonction avec les mĂŞmes valeurs d'entrĂ©e conduit toujours aux mĂŞmes rĂ©sultats.

Analysons la fonction returnZeroFunc() .

At-elle des effets secondaires? Nous venons de découvrir que l'appel de returnZeroFunc() ne lance pas de missiles. Si vous n'appelez pas ce que renvoie cette fonction, rien ne se passera. Par conséquent, nous pouvons conclure que cette fonction n'a pas d'effets secondaires.

Cette fonctionnalité est-elle référentiellement transparente? Autrement dit, retourne-t-il toujours le même lors du passage des mêmes données d'entrée? Nous allons vérifier cela, en profitant du fait que dans le fragment de code ci-dessus, nous avons appelé cette fonction plusieurs fois:

 zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true 

Tout semble bien, mais la fonction returnZeroFunc() n'est pas encore complètement propre. Elle se réfère à une variable qui est en dehors de sa propre portée. Afin de résoudre ce problème, nous réécrivons la fonction:

 // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   function fZero() {       console.log('Launching nuclear missiles');       //              return 0;   }   return fZero; } 

Maintenant, la fonction peut être considérée comme propre. Cependant, dans cette situation, les règles JavaScript jouent contre nous. A savoir, nous ne pouvons plus utiliser l'opérateur === pour vérifier la transparence référentielle d'une fonction. Cela est dû au fait que returnZeroFunc() renverra toujours une nouvelle référence à la fonction. Certes, la transparence des liens peut être vérifiée en examinant le code vous-même. Une telle analyse montrera qu'à chaque appel de fonction, elle renvoie un lien vers la même fonction.

Devant nous se trouve une petite échappatoire soignée. Mais peut-il être utilisé dans de vrais projets? La réponse à cette question est positive. Cependant, avant de parler de la façon de l'utiliser dans la pratique, nous développerons un peu notre idée. À savoir, fZero() à la fonction dangereuse fZero() :

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

Nous allons essayer d'utiliser le zéro renvoyé par cette fonction, mais nous le ferons pour que (jusqu'à présent) une guerre nucléaire ne démarre pas. Pour ce faire, créez une fonction qui prend le zéro renvoyé par la fonction fZero() et y ajoute un:

 // fIncrement :: (() -> Number) -> Number function fIncrement(f) {   return f() + 1; } fIncrement(fZero); //      //   1 

C'est pas de chance ... Nous avons accidentellement déclenché une guerre nucléaire. Réessayons, mais cette fois, nous ne retournerons pas de chiffre. Au lieu de cela, nous renvoyons une fonction qui un jour renvoie un nombre:

 // fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) {   return () => f() + 1; } fIncrement(zero); //   [Function] 

Vous pouvez maintenant respirer facilement. La catastrophe est évitée. Nous poursuivons l'étude. Grâce à ces deux fonctions, nous pouvons créer tout un tas de «nombres possibles»:

 const fOne   = fIncrement(zero); const fTwo   = fIncrement(one); const fThree = fIncrement(two); //   … 

De plus, nous pouvons créer de nombreuses fonctions dont les noms commenceront par f (appelons-les fonctions f*() ), conçues pour fonctionner avec des «nombres possibles»:

 // fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) {   return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) {   return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) {   return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); //    ,   . ! 

Voyez ce que nous avons fait ici? Avec les "nombres possibles", vous pouvez faire la même chose qu'avec les nombres ordinaires. Les mathématiciens appellent cela l' isomorphisme . Un nombre ordinaire peut toujours être transformé en "nombre possible" en le plaçant dans une fonction. Vous pouvez obtenir le "numéro possible" en appelant la fonction. En d'autres termes, nous avons une correspondance entre les nombres réguliers et les "nombres possibles". En fait, cela est beaucoup plus intéressant qu'il n'y paraît. Nous reviendrons bientôt sur cette idée.

La technique ci-dessus utilisant la fonction wrapper est une stratégie valide. Nous pouvons nous cacher derrière les fonctions autant que nécessaire. Et, puisque nous n'avons encore appelé aucune de ces fonctions, toutes, théoriquement, sont pures. Et personne ne déclenche une guerre. Dans le code normal (non lié à la fusée), nous avons en fait besoin d'effets secondaires à la fin. Envelopper tout ce dont nous avons besoin dans une fonction nous permet de contrôler précisément ces effets. Nous choisissons le moment où ces effets apparaissent.

Il convient de noter qu'il n'est pas très pratique d'utiliser des constructions uniformes avec des tas de crochets partout pour déclarer des fonctions. Et la création de nouvelles versions de chaque fonction n'est pas non plus une activité agréable. JavaScript a de grandes fonctions Math.sqrt() comme Math.sqrt() . Ce serait formidable s'il existait un moyen d'utiliser ces fonctions ordinaires avec nos «valeurs en attente». En fait, nous en parlerons maintenant.

Effet Functor


Nous parlerons ici des foncteurs représentés par des objets contenant nos «fonctions différées». Pour représenter le foncteur, nous utiliserons l'objet Effect . Nous allons mettre notre fonction fZero() dans un tel objet. Mais avant de faire cela, nous allons rendre cette fonction un peu plus sûre:

 // zero :: () -> Number function fZero() {   console.log('Starting with nothing');   //  , ,     .   //       .   return 0; } 

Nous décrivons maintenant la fonction constructeur pour créer des objets de type Effect :

 // Effect :: Function -> Effect function Effect(f) {   return {}; } 

Il n'y a rien de particulièrement intéressant ici, nous allons donc travailler sur cette fonctionnalité. Donc, nous voulons utiliser la fonction habituelle fZero() avec l'objet Effect . Pour fournir un tel scénario, nous écrirons une méthode qui accepte une fonction régulière et l'appliquera un jour à notre «valeur en attente». Et nous le ferons sans appeler la fonction Effect . Nous appelons une telle fonction map() . Il porte un tel nom car il crée un mappage entre la fonction habituelle et la fonction d' Effect . Cela peut ressembler à ceci:

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       }   } } 

Maintenant, si vous surveillez de près ce qui se passe, vous pouvez avoir des questions sur la fonction map() . Cela ressemble étrangement à la chanson. Nous reviendrons sur ce problème plus tard, mais pour l'instant nous allons tester ce que nous avons en ce moment en action:

 const zero = Effect(fZero); const increment = x => x + 1; //   . const one = zero.map(increment); 

Alors ... Maintenant, nous n'avons pas l'occasion d'observer ce qui s'est passé ici. Par conséquent, modifions Effect afin, pour ainsi dire, d'avoir l'opportunité de "tirer sur la gâchette":

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }   } } const zero = Effect(fZero); const increment = x => x + 1; //  . const one = zero.map(increment); one.runEffects(); //       //   1 

Si nécessaire, nous pouvons continuer d'appeler la fonction map() :

 const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero)   .map(increment)   .map(double)   .map(cube); eight.runEffects(); //       //   8 

Ici, ce qui se passe commence déjà à devenir plus intéressant. Nous l'appelons un «foncteur». Tout cela signifie que l'objet Effect a une fonction map() et qu'il obéit à certaines règles . Cependant, ce ne sont pas des règles qui interdisent quoi que ce soit. Ces règles concernent ce que vous pouvez faire. Ce sont plutôt des privilèges. Puisque l'objet Effect est un foncteur, il obéit à ces règles. Il s'agit en particulier de la «règle de composition».

Cela ressemble Ă  ceci:

S'il existe un objet Effect nommé e et deux fonctions, f et g , e.map(g).map(f) équivalent à e.map(x => f(g(x))) .

En d'autres termes, deux méthodes map() consécutives équivalent à composer deux fonctions. Cela signifie qu'un objet de type Effect peut effectuer des actions similaires aux suivantes (rappelez-vous l'un des exemples ci-dessus):

 const incDoubleCube = x => cube(double(increment(x))); //       Ramda  lodash/fp      : // const incDoubleCube = compose(cube, double, increment); const eight = Effect(fZero).map(incDoubleCube); 

Lorsque nous faisons ce qui est montré ici, nous sommes assurés d'obtenir le même résultat que nous obtiendrions en utilisant une version de ce code avec un triple appel à map() . Nous pouvons l'utiliser lors de la refactorisation du code, et nous pouvons être sûrs que le code fonctionnera correctement. Dans certains cas, le passage d'une approche à une autre peut même améliorer les performances.

Maintenant, je propose d'arrêter d'expérimenter avec les nombres et de parler de ce qui ressemble davantage au code utilisé dans les projets réels.

▍Méthode de ()


Le constructeur de l'objet Effect prend, comme argument, une fonction. C'est pratique, car la plupart des effets secondaires que nous voulons reporter sont des fonctions. Par exemple, ce sont Math.random() et console.log() . Cependant, vous devez parfois mettre une valeur dans un objet Effect qui n'est pas une fonction. , , window . , . , ( -, , , Haskell pure ):

 // of :: a -> Effect a Effect.of = function of(val) {   return Effect(() => val); } 

, , , -. , , . HTML- . , . . Par exemple:

 window.myAppConf = {   selectors: {       'user-bio':     '.userbio',       'article-list': '#articles',       'user-name':    '.userfullname',   },   templates: {       'greet':  'Pleased to meet you, {name}',       'notify': 'You have {n} alerts',   } }; 

, Effect.of() , Effect :

 const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']); //   Effect('.userbio') 

â–Ť Effect


. , Effect . , getElementLocator() , Effect , . DOM, document.querySelector() — , . :

 // $ :: String -> Effect DOMElement function $(selector) {   return Effect.of(document.querySelector(s)); } 

, , map() :

 const userBio = userBioLocator.map($); //   Effect(Effect(<div>)) 

, , . div , map() , , . , innerHTML , :

 const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML)); //   Effect(Effect('<h2>User Biography</h2>')) 

, . userBio , . , , , . , , Effect('user-bio') . , , , :

 Effect(() => '.userbio'); 

— . :

 Effect(() => window.myAppConf.selectors['user-bio']); 

, map() , ( ). , , $ , :

 Effect(() => $(window.myAppConf.selectors['user-bio'])); 

, :

 Effect(   () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) ); 

Effect.of , :

 Effect(   () => Effect(       () => document.querySelector(window.myAppConf.selectors['user-bio'])   ) ); 

, , , . Effect .

â–Ť join()


? , Effect . , , .

Effect .runEffect() . . , - , , , , . , . join() . Effect , runEffect() , . , .

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }   } } 

, :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .map($)   .join()   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

â–Ť chain()


, .map() , .join() , . , , . , , Effect . , .map() .join() . , , Effect :

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }       chain(g) {           return Effect(f).map(g).join();       }   } } 

chain() - , , Effect ( , ). HTML- :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .chain($)   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

-. . , flatMap . , , — , , join() . Haskell, , bind . , - , , chain , flatMap bind — .

â–Ť Effect


Effect , . . , DOM, , ? , , , . , . — .

 // tpl :: String -> Object -> String const tpl = curry(function tpl(pattern, data) {   return Object.keys(data).reduce(       (str, key) => str.replace(new RegExp(`{${key}}`, data[key]),       pattern   ); }); 

. :

 const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str}); //   Effect({name: 'Mr. Hatter'}); const pattern = win.map(w => w.myAppConfig.templates('greeting')); //   Effect('Pleased to meet you, {name}'); 

, . . ( name pattern ) Effect . tpl() , , Effect .
, map() Effect tpl() :

 pattern.map(tpl); //   Effect([Function]) 

, . map() :

 map :: Effect a ~> (a -> b) -> Effect b 

:

 tpl :: String -> Object -> String 

, map() pattern , ( , tpl() ) Effect .

 Effect (Object -> String) 

pattern Effect . . Effect , . ap() :

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }       chain(g) {           return Effect(f).map(g).join();       }       ap(eff) {            //  -  ap,    ,   eff   (  ).           //    map  ,    eff       (  'g')           //   g,     f()           return eff.map(g => g(f()));       }   } } 

.ap() :

 const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str})); const pattern = win.map(w => w.myAppConfig.templates('greeting')); const greeting = name.ap(pattern.map(tpl)); //   Effect('Pleased to meet you, Mr Hatter') 

, … , , .ap() . , , map() , ap() . , , .

. , . , , , Effect , ap() . , :

 // liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) {   return y.ap(x.map(f));   //      :   // return x.map(f).chain(g => y.map(g)); }); 

liftA2() , , . liftA3() :

 // liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d) const liftA3 = curry(function liftA3(f, a, b, c) {   return c.ap(b.ap(a.map(f))); }); 

, liftA2() liftA3() Effect . , , ap() .

liftA2() :

 const win = Effect.of(window); const user = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str}); const pattern = win.map(w => w.myAppConfig.templates['greeting']); const greeting = liftA2(tpl)(pattern, user); //   Effect('Pleased to meet you, Mr Hatter') 

?


, , , . ? , Effect ap() . , ? ?

: « , , ».

:

  • — ?
  • , Effect , ?

â–Ť


— . , , , . const pattern = window.myAppConfig.templates['greeting']; , , , :

 const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting')); 

— , , , , . . — , , . , , , , , , . , . — . , , , . , .

. .

â–Ť Effect


, , . - Facebook Gmail . ? .

, . . CSV- . . , , , . , . , . , , , .

, . , map() reduce() , . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .

, , . , . Ça ne ressemble à rien? , , , . . , .

TensorFlow , .

TensorFlow, , . «». , , :

 node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2) 

Python, JavaScript. , Effect , add() , ( sess.run() ).

 print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) #  node3:  Tensor("Add_2:0", shape=(), dtype=float32) #  sess.run(node3):  7.0 

, (7.0) , sess.run() . , . , , , .

Résumé


, . , . Effect .
, , , , , . , , . Effect , , , . , .

— . , . , , . . . , .



Chers lecteurs! ?

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


All Articles