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
).
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.