Pensée de style Ramda: immuabilité et objets

1. Premiers pas
2. Combinez les fonctions
3. Utilisation partielle (curry)
4. Programmation déclarative
5. Notation quintessentielle
6. Immuabilité et objets
7. Immuabilité et réseaux
8. Objectifs
9. Conclusion


Ce billet est la sixième partie d'une série d'articles sur la programmation fonctionnelle appelée Ramda Style Thinking.


Dans la cinquième partie, nous avons parlé d'écrire des fonctions dans le style de notation inutile, où l'argument principal avec les données de notre fonction n'est pas spécifié explicitement.


À ce moment-là, nous ne pouvions pas réécrire toutes nos fonctions dans un style sans bit, car nous n'avions pas les outils nécessaires pour cela. Il est temps de les étudier.


Lecture des propriétés des objets


Reprenons l'exemple de la définition des personnes qui ont le droit de vote, que nous avons examiné dans la cinquième partie :


const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

Comme vous pouvez le voir, nous avons créé isCitizen et isEligibleToVote , mais nous ne pouvons pas le faire avec les trois premières fonctions.


Comme nous l'avons appris dans la quatrième partie , nous pouvons rendre nos fonctions plus déclaratives en utilisant equals et gte . Commençons par ceci:


 const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18) 

Pour rendre ces fonctions inutiles, nous avons besoin d'un moyen de construire la fonction afin d'appliquer la variable person à la fin de l'expression. Le problème est que nous devons accéder aux propriétés de la person , maintenant nous savons la seule façon de le faire - et c'est impératif.


accessoire


Heureusement, Ramda vient une fois de plus à notre secours. Il fournit une fonction prop pour accéder aux propriétés des objets.


En utilisant prop , nous pouvons réécrire person.birthCountry à prop('birthCountry', person) . Faisons-le:


 const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

Wow, maintenant ça a l'air bien pire. Mais continuons notre refactoring. Modifions l'ordre des arguments que nous passons à equals pour que l' prop arrive en dernier. equals fonctionne exactement de la même manière en sens inverse, donc nous ne casserons rien:


 const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

Ensuite, utilisons le currying, la propriété naturelle de equals et gte , afin de créer de nouvelles fonctions auxquelles le résultat de l'appel prop s'appliquera:


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person)) 

Cela ressemble toujours à la pire option, mais continuons. Profitons du curry à nouveau pour tous les appels d' prop :


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person)) 

Encore une fois, en quelque sorte pas très. Mais maintenant, nous voyons un schéma familier. Toutes nos fonctions ont la même image f(g(person)) , et comme nous le savons dans la deuxième partie , cela équivaut à compose(f, g)(person) .


Appliquons cet avantage à notre code:


 const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person) 

Maintenant, nous avons quelque chose. Toutes nos fonctions ressemblent à person => f(person) . Et nous savons déjà de la cinquième partie que nous pouvons rendre ces fonctions inutiles.


 const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age')) 

Lorsque nous avons commencé, il n'était pas évident que nos méthodes faisaient deux choses. Ils se sont tournés vers la propriété de l'objet et ont préparé certaines opérations avec sa valeur. Cette refactorisation dans un style inutile a rendu cela très explicite.


Jetons un coup d'œil à certains des autres outils fournis par Ramda pour travailler avec des objets.


cueillir


Lorsque prop lit une propriété d'un objet et renvoie sa valeur, pick lit de nombreuses propriétés de l'objet et ne renvoie un nouvel objet qu'avec elles.


Par exemple, si nous n'avons besoin que des noms et des années des personnes, nous pouvons utiliser pick(['name','age'], person) .


a


Si nous voulons juste savoir que notre objet a une propriété, sans lire sa valeur, nous pouvons utiliser la fonction has pour vérifier ses propriétés, ainsi que hasIn pour vérifier la chaîne prototype: has('name', person) .


chemin


Lorsque prop une propriété d'objet, le chemin va plus loin dans les objets imbriqués. Par exemple, nous voulons extraire le code postal d'une structure plus profonde: path(['address','zipCode'], person) .


Notez que le path plus indulgent que l' prop . path retournera undefined si quelque chose dans le chemin (y compris l'argument d'origine) est null ou undefined , tandis que prop provoquera une erreur dans de telles situations.


propOr / pathOr


propOr et pathOr sont similaires à prop et path combinés avec defaultTo . Ils vous permettent de spécifier une valeur par défaut pour une propriété ou un chemin qui ne peut pas être trouvé dans l'objet étudié.


Par exemple, nous pouvons fournir un espace réservé lorsque nous ne connaissons pas le nom de la personne: propOr('<Unnamed>, 'name', person) . Notez que contrairement à prop , propOr ne provoquera pas d'erreur si la person est null ou undefined ; à la place, il renverra la valeur par défaut.


clés / valeurs


keys renvoie un tableau contenant tous les noms de toutes les propriétés connues de l'objet. Les valeurs renverront les valeurs de ces propriétés. Ces fonctions peuvent être utiles lorsqu'elles sont combinées avec les fonctions d'itération pour les collections, que nous avons découvertes dans la première partie .


Ajouter, mettre à jour et supprimer des propriétés


Nous avons maintenant de nombreux outils pour lire des objets dans un style déclaratif, mais qu'en est-il des changements?


Étant donné que l'immuabilité est importante pour nous, nous ne voulons pas modifier directement les objets. Au lieu de cela, nous voulons retourner de nouveaux objets qui ont changé comme nous le voulons.


Encore une fois, Ramda nous offre de nombreux avantages.


assoc / assocPath


Lorsque nous programmons dans un style impératif, nous pouvons définir ou modifier le nom de la personne via l'opérateur d'affectation: person.name = 'New name' .


Dans notre monde fonctionnel et immuable, nous pouvons utiliser assoc à la place: const updatedPerson = assoc('name', 'newName', person) .


assoc renvoie un nouvel objet avec une valeur de propriété ajoutée ou mise à jour, laissant l'objet d'origine inchangé.


Nous avons également à notre disposition assocPath pour mettre à jour la propriété jointe: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person) .


dissoc / dissocPath / omit


Qu'en est-il de la suppression des propriétés? Impérativement, nous pourrions vouloir dire delete person.age . Dans Ramda, nous utiliserons dissoc : `const updatedPerson = dissoc ('age', person)


dissocPath est à peu près la même chose, mais fonctionne sur des structures d'objets plus profondes: dissocPath(['address', 'zipCode'], person) .


Et nous avons également omit , ce qui peut supprimer plusieurs propriétés à la fois: const updatedPerson = omit(['age', 'birthCountry'], person) .


Veuillez noter que pick et omit peu similaires et se complètent très bien. Ils sont très pratiques pour la liste blanche (enregistrer uniquement un certain ensemble de propriétés à l'aide de pick ) et les listes noires (pour omit débarrasser de certaines propriétés en utilisant omit ).


Transformation d'objet


Nous en savons maintenant assez pour travailler avec des objets dans un style déclaratif et immuable. Écrivons une fonction celebrBirthday qui met à jour l'âge de la personne à son anniversaire.


 const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person) 

Il s'agit d'un schéma très courant. Au lieu de mettre à jour la propriété avec une nouvelle valeur, nous voulons vraiment changer la valeur en appliquant la fonction à l'ancienne valeur, comme nous l'avons fait ici.


Je ne connais pas de bonne façon d'écrire ceci avec moins de duplication et dans un style moins rigoureux, avec ces outils que nous avons appris plus tôt.


Ramda nous sauve encore une fois avec la fonction évoluer . evolve accepte un objet et vous permet de spécifier des fonctions de transformation pour les propriétés que nous voulons changer. Réfractons celebrateBirthday sur l'utilisation d' evolve :


 const celebrateBirthday = evolve({ age: inc }) 

Ce code indique que nous convertirons l'objet spécifié (qui ne s'affiche pas en raison du style brutal) en créant un nouvel objet avec les mêmes propriétés et valeurs, mais la propriété age sera obtenue en appliquant inc à la valeur d'origine de la propriété age .


evolve peut transformer de nombreuses propriétés à la fois, et même à plusieurs niveaux d'imbrication. La transformation de l'objet peut avoir la même image que l'objet mutable aura, et evolve passera récursivement entre les structures, en utilisant les fonctions de transformation sous la forme spécifiée.


Notez que evolve n'ajoute pas de nouvelles propriétés; si vous spécifiez une transformation pour une propriété qui ne se produit pas dans l'objet en cours de traitement, evolve ignorera simplement.


J'ai trouvé evolve rapidement un cheval de bataille dans mes applications.


Fusionner des objets


Parfois, vous devez combiner deux objets ensemble. Un cas typique est lorsque vous avez une fonction qui prend des options nommées et que vous souhaitez les combiner avec les options par défaut. Ramda fournit une fonction de fusion à cet effet.


 function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) } 

merge renvoie un nouvel objet contenant toutes les propriétés et valeurs des deux objets. Si les deux objets ont la même propriété, la valeur du deuxième argument sera obtenue.


La présence de cette règle avec un deuxième argument gagnant rend utile l'utilisation de la merge comme outil autonome, mais moins significatif dans les situations de convoyeur. Dans ce cas, vous devez souvent préparer une série de transformations pour un objet, et l'une de ces transformations est l'union de certaines nouvelles valeurs de propriété. Dans ce cas, vous voudrez que le premier argument gagne au lieu du second.


Essayer d'utiliser simplement merge(newValues) dans le pipeline ne donnera pas ce que nous aimerions obtenir.


Pour cette situation, je crée généralement mon propre utilitaire appelé reverseMerge . Il peut être écrit comme const reverseMerge = flip(merge) . L'appel flip échange les deux premiers arguments de la fonction qui lui est applicable.


merge effectue une fusion de surface. Si les objets, lorsqu'ils sont combinés, ont une propriété dont la valeur est un sous-objet, ces sous-objets ne fusionnent pas. Ramda n'a pas actuellement de capacité de fusion profonde (L' article d'origine que je traduis contient déjà des informations obsolètes sur ce sujet. Aujourd'hui, Ramda a des fonctions telles que mergeDeepLeft , mergeDeepRight pour la fusion récursive d'objets en profondeur et d'autres méthodes de fusion ).


Notez que la merge n'accepte que deux arguments. Si vous souhaitez combiner plusieurs objets en un seul, vous pouvez utiliser mergeAll , qui prend un tableau d'objets à combiner.


Conclusion


Aujourd'hui, nous avons un merveilleux ensemble d'outils pour travailler avec des objets dans un style déclaratif et immuable. Nous pouvons désormais lire, ajouter, mettre à jour, supprimer et transformer des propriétés dans des objets sans changer les objets d'origine. Et nous pouvons faire toutes ces choses dans un style qui facilite la combinaison des fonctions les unes avec les autres.


Suivant


Nous pouvons maintenant travailler avec des objets dans un style immuable, mais qu'en est-il des tableaux? "Immunité et tableaux" nous dira quoi faire avec eux.

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


All Articles