Combinaison de fonctions

Dans le premier chapitre, j'ai présenté Ramda et quelques concepts basiques de programmation fonctionnelle, comme les fonctions, les fonctions pures, et l'immutabilité. J'ai alors suggéré qu'une bonne manière de commencer était d'utiliser les fonction itération-collection comme forEach, map, select, et consorts.

Combinaisons simples

Une fois que vous vous êtes fait à l'idée de passer des fonctions à d'autres fonctions, vous pourriez tomber sur des situations où vous voudriez combiner plusieurs fonctions.

Ramda fournit plusieurs fonctions pour les combinaisons simples. Voyons-en quelques unes.

Complement

Dans le précédent chapitre, nous avons utilisé find pour trouver le premier nombre pair d'une liste:

const isEven = x => x % 2 === 0

find(isEven, [1, 2, 3, 4]) // --> 2

Et si nous voulions trouver le premier nombre impair à la place? Nous pouvons toujours écrire une fonction isOdd et l'utiliser, mais nous savons qu'un nombre qui n'est pas pair est impair. Réutilisons notre fonction isEven.

Ramda fournit une fonction d'ordre supérieur (NDT: higher-order function), complement, qui prend une autre fonction et retourne une nouvelle fonction true quand la fonction original renvoie une valeur fausse (NDT: falsy value), et false quand la fonction originale renvoie une valeur vraie (NDT: truthy value).

const isEven = x => x % 2 === 0

find(complement(isEven), [1, 2, 3, 4]) // --> 1

C'est encore mieux de donner à la fonction complementée son propre nom, pour qu'on puisse la réutiliser:

const isEven = x => x % 2 === 0
const isOdd = complement(isEven)

find(isOdd, [1, 2, 3, 4]) // --> 1

Remarquez que complement fait la même chose sur les fonctions que ce que l'opérateur ! (non) fait sur les valeurs.

Both/Either

Disons que nous travaillons sur un système de vote. Étant donnée une personne, nous voudrions être capable de déterminer si cette personne a le droit de vote. Selon nos connaissances actuelles, une personne doit avoir au moins 18 ans et être citoyen pour avoir le droit de vote. Quelqu'un est un citoyen s'il est né dans le pays ou s'il l'est devenu plus tard en se faisant naturaliser.

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

const isCitizen = person => wasBornInCountry(person) || wasNaturalized(person)

const isEligibleToVote = person => isOver18(person) && isCitizen(person)

Ce que nous avons écrit ci-dessus fonctionne, mais Ramda fournit quelques fonctions pratiques pour nous aider à rendre ce code un peu plus propre.

both prend deux autres fonctions et renvoie une nouvelle fonction qui retourne true si les deux fonctions renvoient une valeur vraie quand elles sont appliquées à leurs arguments, et false sinon.

either prend deux autres fonctions et renvoie une nouvelle fonction qui retourne true si l'une ou l'autre des fonctions renvoie une valeur vrai quand elles sont appliquées à leurs arguments, et false sinon.

En utilisant ces deux fonctions, nous pouvons simplifier isCitizen et isEligibleToVote:

const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)

Remarquez que both fait la même chose avec des fonctions que ce que l'opérateur && fait pour des valeurs, et que either fait la même chose avec des fonctions que ce que l'opérateur || fait avec des valeurs.

Ramda fournit aussi allPass et anyPass, qui prennent un tableau de fonctions de taille quelconque. Comme leurs noms le suggèrent, allPass fonctionne comme both, et anyPass comme either.

Pipelines

Quelquefois nous voulons appliquer plusieurs fonctions à des données, comme à travers un pipeline. Par exemple, nous pourrions vouloir prendre deux nombres, multiplier l'un par l'autre, ajouter un et mettre le résultat au carré. Nous pourrions l'écrire comme ceci:

const multiply = (a, b) => a * b
const addOne = x => x + 1
const square = x => x * x

const operate = (x, y) => {
  const product = multiply(x, y)
  const incremented = addOne(product)
  const squared = square(incremented)

  return squared
}

operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169

Notez que chaque opération est appliquée au résultat de la précédente.

pipe

Ramda fournit la fonction pipe, qui prend une liste d'une ou plusieurs fonctions, et renvoie une nouvelle fonction.

Cette nouvelle fonction prendre le même nombre d'arguments que la première fonction qu'on a donné. Elle fait alors passer ces arguments à travers un "tuyau" (NDT: pipes) composé de toutes les fonctions de la liste. Elle applique la première fonction aux arguments, passe son résultat à la deuxième fonction, etc. Le résultat de la dernière fonction est le résultat de l'appel à pipe.

Remarquez que toutes les fonctions après la première ne doivent prendre qu'un seul argument.

Sachant cela, nous pouvons utiliser pipe pour simplifier notre fonction operate:

const operate = pipe(
  multiply,
  addOne,
  square
)

Quand nous appelons operate(3,4), pipe passe 3 et 4 à la fonction multiply, dont le résultat est 12. Elle passe ce 12 à addOne, qui renvoie 13. Elle passe alors ce 13 à square, qui retourne 169, et cela devient le résultat final d'operate.

compose

Une autre façon pour écrire notre fonction operate originale aurait été d'intégrer (NDT: to inline) toutes les variables temporaires:

const operate = (x, y) => square(addOne(multiply(x, y)))

C'est bien plus compact, mais un peu plus dur à lire. Dans cette forme pourtant, on a envie de le réécrire en utilisant la fonction compose de Ramda.

compose fonctionne exactement de la même manière que pipe, excepté qu'elle applique les fonctions de droite à gauche, et non de gauche à droite. Écrivons operate avec compose:

const operate = compose(
  square,
  addOne,
  multiply
)

C'est la même chose que le pipe précédent, mais avec les fonctions dans l'ordre inverse. En fait, la fonction compose de Ramda est écrite avec pipe.

Je pense toujours compose de cette manière: compose(f, g)(value) est équivalent à f(g(value)).

Comme avec pipe, remarquez que toutes les fonctions sauf la dernière ne doivent prendre qu'un seul paramètre.

compose ou pipe ?

Je crois que pipe est plus simple à comprendre quand on a l'habitude de la programmation impérative, parce qu'on lit les fonctions de gauche à droite. Par contre, compose est un peu plus facile à traduire en fonction imbriquée (NDT: ?) comme je l'ai montré plus haut.

Je n'ai pas encore trouvé de bonne règle pour décider quand il faut préférer composeet quand il faut préférer pipe. Comme ils sont équivalents en Ramda, choisir l'une ou l'autre n'a probablement pas grande importance. Choisissez simplement celle qui ce lit le mieux dans le contexte.

Conclusion

En combinant plusieurs fonctions d'une certaine manière, on peut commencer à écrire des fonctions plus puissantes (NDT: powerful).

Chapitre suivant

Vous avez peut-être remarqué que nous avons la plupart du temps ignoré les arguments des fonctions, en les combinant. Nous ne fournissons les arguments qu'au moment où nous appelons la fonction combinée (NDT: combined function, peut-être «fonction combinaison» serait-il meilleur, mais ça ne sonne pas très bien).

C'est habituel en programmation fonctionnelle, et nous allons en parler beaucoup plus dans le prochain chapitre, Application partielle. Nous y parlons aussi de la manière de combiner des fonctions qui prennent plus qu'un argument.

results matching ""

    No results matching ""