Cet article a été révisé par des pairs par Craig Bilner et Adrian Sandu. Merci à tous les pairs examinateurs de SitePoint pour avoir fait du contenu SitePoint le meilleur possible!
La programmation fonctionnelle et les données immuables sont un objectif actuel pour de nombreux développeurs JavaScript alors qu'ils essaient de trouver des moyens de rendre leur code plus simple et plus facile à raisonner.
Bien que JavaScript ait toujours pris en charge certaines techniques de programmation fonctionnelle, elles ne sont devenues vraiment devenues que ces dernières années et, traditionnellement, il n'y a pas eu de support natif pour les données immuables non plus. JavaScript apprend toujours beaucoup sur les deux et les meilleures idées proviennent des langues qui ont déjà essayé et testé ces techniques.
Dans un autre coin du monde de la programmation, Clojure est un langage de programmation fonctionnel dédié à une véritable simplicité, en particulier en ce qui concerne les structures de données. Mori est une bibliothèque qui nous permet d'utiliser les structures de données persistantes de Clojure directement à partir de JavaScript.
Cet article explorera la justification derrière la conception de ces structures de données et examinera certains modèles pour les utiliser pour améliorer nos applications. Nous pourrions également considérer cela comme le premier tremplin pour les développeurs JavaScript intéressé par la programmation avec Clojure ou Clojurescript.
Clojure fait une distinction entre les valeurs persistantes qui ne peuvent pas être modifiées et des valeurs transitoires qui ont des durées de vie temporelles entre les mutations. Les tentatives de modification des structures de données persistantes évitent de muter les données sous-jacentes en renvoyant une nouvelle structure avec les modifications appliquées.
Cela peut aider à voir à quoi ressemblerait cette distinction dans un langage de programmation théorique.
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
Nous pouvons voir que la liste transitoire a été mutée lorsque nous avons poussé une valeur dessus. A et B pointent la même valeur mutable. En revanche, appeler Push sur la liste persistante a renvoyé une nouvelle valeur et nous pouvons voir que C et D pointent vers des listes différentes de discrets.
Ces structures de données persistantes ne peuvent pas être mutées, ce qui signifie qu'une fois que nous avons une référence à une valeur, nous avons également une garantie qu'elle ne sera jamais modifiée. Ces garanties nous aident généralement à écrire du code plus sûr et plus simple. Par exemple, une fonction qui prend des structures de données persistantes car les arguments ne peuvent pas les muter et, par conséquent, si la fonction veut communiquer un changement significatif, il doit provenir de la valeur de retour. Cela conduit à l'écriture de fonctions pures référentiellement transparentes, qui sont plus faciles à tester et à optimiser.
Plus simplement, les données immuables nous obligent à écrire plus de code fonctionnel.
MORI utilise le compilateur Clojurescript pour compiler les implémentations des structures de données de la bibliothèque standard de Clojure à JavaScript. Le compilateur émet un code optimisé, ce qui signifie que sans considération supplémentaire, il n'est pas facile de communiquer avec Clojure compilé de JavaScript. Mori est la couche de considération supplémentaire.
Tout comme Clojure, les fonctions de Mori sont séparées des structures de données sur lesquelles elles opèrent, ce qui contraste avec les tendances orientées objet de JavaScript. Nous constaterons que cette différence modifie la direction que nous écrivons du code.
<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>
MORI utilise également le partage structurel pour apporter des modifications efficaces aux données en partageant autant de structure d'origine que possible. Cela permet aux structures de données persistantes d'être presque aussi efficaces que celles transitoires régulières. Les implémentations de ces concepts sont couvertes de manière beaucoup plus détaillée dans cette vidéo.
Pour commencer, imaginons que nous essayons de retrouver un bug dans une base de code JavaScript dont nous avons hérité. Nous lisons le code en essayant de comprendre pourquoi nous nous sommes retrouvés avec la mauvaise valeur pour la communion.
<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
Quelle est la valeur de la communion lorsqu'elle est connectée à la console?
Sans exécuter le code, ni lire la définition de DelePerson (), il n'y a aucun moyen de savoir. Ce pourrait être un tableau vide. Il pourrait avoir trois nouvelles propriétés. Nous espérons qu'il s'agit d'un tableau avec le deuxième élément supprimé, mais parce que nous sommes passés dans une structure de données mutable, il n'y a aucune garantie.
Encore pire, la fonction pourrait maintenir une référence et la muter de manière asynchrone à l'avenir. Toutes les références à la communion à partir d'ici vont travailler avec une valeur imprévisible.
Comparez cela à une alternative avec Mori.
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
quelle que soit la mise en œuvre de DelePerson (), nous savons que le vecteur d'origine sera enregistré, simplement parce qu'il existe une garantie qu'elle ne peut pas être mutée. Si nous voulons que la fonction soit utile, il doit renvoyer un nouveau vecteur avec l'élément spécifié supprimé.
Comprendre le flux à travers les fonctions qui fonctionnent sur des données immuables est facile, car nous savons que leur seul effet sera de dériver et de renvoyer une valeur immuable distincte.
Les fonctions fonctionnant sur des données mutables ne renvoient pas toujours les valeurs, elles peuvent muter leurs entrées et parfois il est laissé au programmeur de reprendre la valeur de l'autre côté.
Plus simplement, les données immuables appliquent une culture de prévisibilité.
Nous allons voir comment nous pouvons utiliser MORI pour construire un éditeur de pixels avec une fonctionnalité d'annulation. Le code suivant est disponible sous forme de codepen que vous pouvez également trouver au pied de l'article.
Nous supposerons que vous suivez soit sur Codepen, soit vous travaillez dans un environnement ES2015 avec MORI et le HTML.
<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>
Commençons par la destruction des fonctions dont nous aurons besoin de l'espace de noms MORI.
<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
Il s'agit principalement d'une préférence stylistique. Vous pouvez également utiliser l'une des fonctions de MORI en y accédant directement sur l'objet MORI (par exemple mori.list ()).
La première chose que nous allons faire est de configurer une fonction d'assistance pour visualiser nos structures de données persistantes. La représentation interne de Mori n'a pas beaucoup de sens dans une console, nous allons donc utiliser la fonction TOJS () pour les convertir en un format compréhensible.
<span>import <span>{ vector, hashMap }</span> from 'mori'; </span> <span>const fellowship = vector( </span> <span>hashMap( </span> <span>"name", "Mori", </span> <span>"race", "Hobbit" </span> <span>), </span> <span>hashMap( </span> <span>"name", "Poppin", </span> <span>"race", "Hobbit" </span> <span>) </span><span>) </span> <span>const newFellowship = deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
Nous pouvons utiliser cette fonction comme alternative à Console.log () lorsque nous devons inspecter les structures de données de Mori.
Ensuite, nous allons configurer certaines valeurs de configuration et une fonction d'utilité.
<span><span><span><div</span>></span> </span> <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span> id<span>="container"</span>></span> </span> <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span>></span> </span> <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span> </span><span><span><span></div</span>></span> </span>
J'espère que vous avez remarqué que notre fonction TO2D () renvoie un vecteur. Les vecteurs sont un peu comme les tableaux JavaScript et prennent en charge un accès aléatoire efficace.
Nous utiliserons notre fonction TO2D () pour créer une séquence de coordonnées qui représentera tous les pixels sur la toile.
<span>const { </span> list<span>, vector, peek, pop, conj, map, assoc, zipmap, </span> range<span>, repeat, each, count, intoArray, toJs </span><span>} = mori; </span>
Nous utilisons la fonction Range () pour générer une séquence de nombres entre 0 et hauteur * Largeur (dans notre cas 100) et nous utilisons MAP () pour le transformer en une liste de coordonnées 2D avec notre TO2D () fonction d'assistance.
Cela pourrait aider à visualiser la structure des coordonnées.
<span>const log = (<span>...args</span>) => { </span> <span>console.log(...args.map(toJs)) </span><span>}; </span>
C'est une séquence unidimensionnelle de vecteurs de coordonnées.
à côté de chaque coordonnée, nous voulons également stocker une valeur de couleur.
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
Nous utilisons la fonction répétitive () pour créer une séquence infinie de chaînes '#fff'. Nous n'avons pas à nous soucier de ce remplissage de la mémoire et de planter notre navigateur, car les séquences MORI prennent en charge Évaluation paresseuse . Nous ne calculerons les valeurs des éléments de la séquence que lorsque nous les demandons plus tard.
Enfin, nous voulons combiner nos coordonnées avec nos couleurs sous la forme d'une carte de hachage.
<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>
Nous utilisons la fonction zipmap () pour créer une carte de hachage, avec les coords en tant que touches et les couleurs sous forme de valeurs. Encore une fois, cela pourrait aider à visualiser la structure de nos données.
<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
Contrairement aux objets de JavaScript, les cartes de hachage de Mori peuvent prendre n'importe quel type de données comme clé.
Pour changer la couleur de Pixel, nous allons associer l'une des coordonnées de notre carte de hachage avec une nouvelle chaîne. Écrivons une fonction pure qui colore un seul pixel.
<span>import <span>{ vector, hashMap }</span> from 'mori'; </span> <span>const fellowship = vector( </span> <span>hashMap( </span> <span>"name", "Mori", </span> <span>"race", "Hobbit" </span> <span>), </span> <span>hashMap( </span> <span>"name", "Poppin", </span> <span>"race", "Hobbit" </span> <span>) </span><span>) </span> <span>const newFellowship = deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
Nous utilisons les coordonnées x et y pour créer un vecteur de coordonnées que nous pouvons utiliser comme clé, puis nous utilisons Assoc () pour associer cette clé à une nouvelle couleur. N'oubliez pas que parce que la structure des données est persistante, la fonction Assoc () renverra une carte de hachage nouvelle , plutôt que de muter l'ancienne.
Maintenant, nous avons tout ce dont nous avons besoin pour dessiner une image simple sur une toile. Créons une fonction qui prend une carte de hachage des coordonnées contre les pixels et les attire sur un renduContext2d.
<span><span><span><div</span>></span> </span> <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span> id<span>="container"</span>></span> </span> <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span> </span><span><span><span></div</span>></span> </span><span><span><span><div</span>></span> </span> <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span> </span><span><span><span></div</span>></span> </span>
Prenons une minute pour comprendre ce qui se passe ici.
Nous utilisons chacun () pour itérer sur notre carte de hachage Pixels. Il passe chaque clé et valeur (ensemble en tant que séquence) dans la fonction de rappel comme p. Ensuite, nous utilisons la fonction enRay () pour la convertir en tableaux qui peuvent être détruits, afin que nous puissions choisir les valeurs que nous voulons.
<span>const { </span> list<span>, vector, peek, pop, conj, map, assoc, zipmap, </span> range<span>, repeat, each, count, intoArray, toJs </span><span>} = mori; </span>
Enfin, nous utilisons des méthodes de toile pour dessiner un rectangle coloré sur le contexte lui-même.
<span>const log = (<span>...args</span>) => { </span> <span>console.log(...args.map(toJs)) </span><span>}; </span>
Maintenant, nous devons faire un peu de plomberie juste pour rassembler toutes ces pièces et fonctionner.
<span>// the dimensions of the canvas </span><span>const [height, width] = [20, 20]; </span> <span>// the size of each canvas pixel </span><span>const pixelSize = 10; </span> <span>// converts an integer to a 2d coordinate vector </span><span>const to2D = (i) => vector( </span> i <span>% width, </span> <span>Math.floor(i / width) </span><span>); </span>
Nous allons nous procurer le canevas et l'utiliser pour créer un contexte pour rendre notre image. Nous redimensions également l'IT de manière appropriée pour refléter nos dimensions.
Enfin, nous passerons notre contexte avec nos pixels à dessiner par la méthode de peinture. Avec un peu de chance, votre toile devrait être rendue sous forme de pixels blancs. Pas la révélation la plus excitante, mais nous nous rapprochons.
Nous voulons écouter des événements de clic et les utiliser pour modifier la couleur d'un pixel spécifique avec notre fonction Draw () de plus tôt.
<span>const coords = map(to2D, range(height * width)); </span>
Nous attachons un écouteur de clic à notre canevas et utilisons les coordonnées d'événements pour déterminer quel pixel de dessiner. Nous utilisons ces informations pour créer une nouvelle carte de hachage Pixel avec notre fonction Draw (). Ensuite, nous peignons cela dans notre contexte et écrasons le dernier cadre que nous avons dessiné.
À ce stade, nous pouvons dessiner des pixels noirs dans la toile et chaque trame sera basée sur la précédente, créant une image composite.
Pour mettre en œuvre une annulation, nous voulons stocker chaque révision historique sur la carte de hachage Pixel, afin que nous puissions les récupérer à nouveau à l'avenir.
<span>// transient list </span>a <span>= [1, 2, 3]; </span>b <span>= a.push(4); </span><span>// a = [1, 2, 3, 4] </span><span>// b = [1, 2, 3, 4] </span> <span>// persistent list </span>c <span>= #[1, 2, 3] </span>d <span>= c.push(4); </span><span>// c = #[1, 2, 3] </span><span>// d = #[1, 2, 3, 4] </span>
Nous utilisons une liste pour stocker les différentes «cadres» que nous avons dessinés. Les listes prennent en charge l'ajout efficace à la tête et o (1) la recherche pour le premier élément, ce qui les rend idéales pour représenter les piles.
Nous devrons modifier notre écouteur Click pour travailler avec notre pile de trame.
<span>// standard library </span><span>Array(1, 2, 3).map(x => x * 2); </span><span>// => [2, 4, 6] </span> <span>// mori </span><span>map(x => x * 2, vector(1, 2, 3)) </span><span>// => [2, 4, 6] </span>
Nous utilisons la fonction peek () pour obtenir le cadre en haut de la pile. Ensuite, nous l'utilisons pour créer un nouveau cadre avec la fonction Draw (). Enfin, nous utilisons conj () pour conjoin le nouveau cadre sur le haut de la pile de trame.
Bien que nous modifions l'état local (frame = conj (cadres, newFrame)), nous ne mutrions pas de données.
Enfin, nous devons implémenter un bouton d'annulation pour faire éclater le cadre supérieur de notre pile.
<span>const fellowship = [ </span> <span>{ </span> <span>title: 'Mori', </span> <span>race: 'Hobbit' </span> <span>}, </span> <span>{ </span> <span>title: 'Poppin', </span> <span>race: 'Hobbit' </span> <span>} </span><span>]; </span> <span>deletePerson(fellowship, 1); </span><span>console.log(fellowship); </span>
Lorsque le bouton Annuler est cliqué, nous vérifions s'il y a actuellement des cadres à annuler, utilisez la fonction pop () pour remplacer les cadres par une nouvelle liste qui n'inclut plus le cadre supérieur.
Enfin, nous passons le cadre supérieur sur la nouvelle pile à notre fonction peinture () pour refléter les modifications. À ce stade, vous devriez être en mesure de dessiner et d'annuler les modifications de la toile.
Voici ce que nous nous retrouvons:
Voir les pixels de Pen Mori par SitePoint (@SitePoint) sur Codepen.
Voici une liste d'idées pour les façons d'améliorer cette application:
Nous avons à peine rayé la surface de ce qui est possible, mais j'espère que vous en aurez suffisamment vu pour valoriser l'importance de l'appariement des données persistantes avec un ensemble puissant de fonctions simples.
L'immuabilité en JavaScript se réfère à l'état d'un objet qui ne peut pas être modifié après sa création. Cela signifie qu'une fois qu'une variable se voit attribuer une valeur, elle ne peut pas être modifiée. Ce concept est crucial dans la programmation fonctionnelle car elle aide à éviter les effets secondaires et rend votre code plus prévisible et plus facile à comprendre. Il améliore également les performances de votre application en permettant une récupération efficace des données et une utilisation de la mémoire.
MORI est une bibliothèque qui fournit un ensemble de Structures de données persistantes en JavaScript. Ces structures de données sont immuables, ce qui signifie qu'elles ne peuvent pas être modifiées une fois qu'elles sont créées. Cela aide à maintenir l'intégrité des données et évite les modifications accidentelles. MORI fournit également un riche ensemble d'utilitaires de programmation fonctionnelle qui facilite la manipulation de ces structures de données.
Tandis que JavaScript JavaScript Fournit des méthodes pour gérer les données immuables, MORI offre un moyen plus efficace et robuste de le faire. Les structures de données persistantes de Mori sont plus rapides et consomment moins de mémoire que les méthodes JavaScript natives. De plus, MORI fournit une large gamme d'utilisations de programmation fonctionnelle qui ne sont pas disponibles en JavaScript.
L'immuabilité peut améliorer considérablement les performances d'une application . Étant donné que les objets immuables ne peuvent pas être modifiés une fois créés, ils peuvent être réutilisés en toute sécurité sur plusieurs appels de fonction sans risque d'être modifiés. Cela conduit à une utilisation efficace de la mémoire et à une récupération des données plus rapide, améliorant ainsi les performances globales de l'application.
Les structures de données mutables sont celles qui peuvent être changé après leur création. D'un autre côté, les structures de données immuables ne peuvent pas être modifiées une fois qu'elles sont créées. Toute opération sur une structure de données immuables se traduit par une nouvelle structure de données.
MORI fournit un riche ensemble d'utilitaires de programmation fonctionnelle pour manipuler les données. Ces services publics vous permettent d'effectuer diverses opérations comme la carte, la réduction, le filtre, etc., sur les structures de données sans modifier les données d'origine.
Structures de données persistantes Dans MORI, les structures de données immuables qui préservent la version précédente des données lorsqu'elles sont modifiées. Cela signifie que chaque fois que vous effectuez une opération sur une structure de données persistante, une nouvelle version de la structure de données est créée et l'ancienne version est conservée.
MORI assure l'intégrité des données en fournissant des structures de données immuables. Étant donné que ces structures de données ne peuvent pas être modifiées une fois créées, le risque de modification accidentelle des données est éliminé. Cela aide à maintenir l'intégrité des données.
La programmation fonctionnelle avec MORI en JavaScript offre plusieurs avantages. Cela rend votre code plus prévisible et plus facile à comprendre en évitant les effets secondaires. Il améliore également les performances de votre application en permettant une récupération efficace des données et une utilisation de la mémoire.
Pour commencer à utiliser MORI dans vos projets JavaScript, vous, vous Besoin d'inclure la bibliothèque MORI dans votre projet. Vous pouvez le faire en l'installant via NPM ou en l'incluant directement dans votre fichier HTML. Une fois la bibliothèque incluse, vous pouvez commencer à utiliser les fonctions et les structures de données de Mori dans votre code.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!