Application partielle
Dans le chapitre précédent, nous avons parlé de combiner les fonctions de différentes manières, en finissant par les fonctions compose
et pipe
qui nous ont permis d'appliquer une série de fonctions en pipeline.
Dans ce chapitre, nous avons regardé des pipelines simples de fonctions qui prenaient un seul paramètre. Mais si nous voulons utiliser des fonctions avec plus d'un paramètre?
Par exemple, disons que nous avons une collection de livres et que nous voulons trouver les titres de tous les livres publiés pendant une année particulière. Écrivons cela en utilisant seulement les fonctions d'itération de collection de Ramda:
const publishedInYear = (book, year) => book.year === year
const titlesForYear = (books, year) => {
const selected = filter(book => publishedInYear(book, year), books)
return map(book => book.title, selected)
}
Ce serait bien de combiner filter
et map
dans un pipeline, mais nous ne savons pas encore comment, car filter
et map
prennent toutes les deux deux paramètres.
De même, on aimerait ne pas utiliser de fonction fléchée dans filter
. Attaquons-nous à ce problème d'abord, parce qu'il va nous apprendre des choses que nous utiliserons pour créer le pipeline.
Fonctions d'ordre supérieur
Dans le premier chapitre, nous avons parlé des fonctions en tant que constructions de première classe (NDT: first-class constructs). Les fonctions de première classe peuvent être passées en paramètres à d'autres fonctions et retournées comme résultats d'autres fonctions. Nous avons déjà passé des fonctions en paramètres, mais pas encore retourné des fonctions.
Les fonctions qui prennent ou retournent d'autres fonctions sont connues comme des fonctions d'ordre supérieur.
Dans l'exemple ci-dessus, nous passons une fonction fléchée à filter
: book => publishedInYear(book, year)
, et nous voudrions essayer de nous débarrasser de la flèche. Pour ce faire, nous avons besoin d'une fonction qui prend un book
et renvoie true
si le livre a été publié dans une année donnée. Mais nous avons aussi besoin de lui faire connaître l'année, pour la rendre souple (NDT: flexible).
Il nous faut changer publishedInYear
en une fonction qui renvoie une autre fonction. Je vais l'écrire avec la syntaxe de fonction entière (NDT: full function syntax) pour que vous puissiez voir ce qui se passe, puis je vous montrerai la version courte, en utilisant la syntaxe fléchée:
// Full function version:
function publishedInYear(year) {
return function(book) {
return book.year === year
}
}
// Arrow function version:
const publishedInYear = year => book => book.year === year
Avec cette nouvelle version de publishedInYear
, nous pouvons réécrire l'appel à notre filtre, en éliminant la fonction fléchée:
const publishedInYear = year => book => book.year === year
const titlesForYear = (books, year) => {
const selected = filter(publishedInYear(year), books)
return map(book => book.title, selected)
}
Maintenant, quand nous appelons filter
, publishedInYear(year)
est évaluée immédiatement, retournant une fonction qui prend un book
, ce qui est exactement ce dont filter
a besoin.
Application partielle de fonctions
Nous pouvons réécrire toute fonction multi-arguments de cette manière si nous le souhaitons, mais nous n'avons pas la main sur toutes les fonctions que nous voudrions utiliser. De plus, nous pourrions vouloir utiliser des fonctions multi-arguments de la manière habituelle.
Par exemple, si nous avions un autre programme qui voulait seulement vérifier qu'un livre a été publié une année donnée, nous aimerions écrire publishedInYear(book, 2012)
, mais nous ne pouvons plus le faire. À la place, nous devons écrire publishedInYear(2012)(book)
. C'est moins lisible et plus embêtant.
Heureusement, Ramda propose deux fonctions pour nous aider: partial
et partialRight
.
Ces deux fonctions permettent d'appeler une fonction quelconque avec moins d'arguments qu'elle n'en réclame. Elles renvoient toutes les deux une nouvelle fonction qui prend les paramètres manquants et appelle la fonction originale une fois que tous les arguments ont été fournis.
La différence entre partial
et partialRight
tient dans le fait que nous donnons les arguments en commençant par le paramètre le plus à gauche ou le plus à droite de la fonction originale.
Retournons à notre exemple originel et utilisons une de ces fonctions au lieu de réécrire publishedInYear
. Comme nous ne voulons passer que l'année, et que c'est l'argument le plus à droite, nous allons utiliser partialRight
.
const publishedInYear = (book, year) => book.year === year
const titlesForYear = (books, year) => {
const selected = filter(partialRight(publishedInYear, [year]), books)
return map(book => book.title, selected)
}
Si nous avions d'abord écrit une fonction publishedInYear
prenant (year, book)
au lieu de (book, year)
, nous aurions pris partial
à la place de partialRight
.
Remarquez que les paramètres fournis à partial
et partialRight
doivent toujours se trouver dans un tableau, même quand il n'y en qu'un. Je ne saurais vous dire combien de fois je l'ai oublié, et obtenu un message d'erreur déroutant.
First argument to _arity must be a non-negative integer no greater than ten
Curryfication
Devoir utiliser partial
et partialRight
partout rend la programmation verbeuse et fastidieuse. Mais devoir appeler des fonctions multi-arguments comme des séries de fonctions à un seul argument est lourd aussi.
Heureusement, Ramda nous offre la solution: curry.
La curryfication est un autre concept de la programmation fonctionnelle. Techniquement, une fonction curryfiée est toujours une série de fonctions à un seul paramètre, ce qui est exactement ce dont je me plaignais. Dans les langages fonctionnels purs, la syntaxe rend cela transparent.
Mais parce que Ramda est une bibliothèque JavaScript, et que JavaScript n'a pas de syntaxe appropriée pour appeler une série de fonctions à un seul argument, les auteurs ont assoupli un peu la définition classique de la curryfication.
En Ramada, une fonction curryfiée peut être appelée avec une sous-partie de ses arguments, et retourne une nouvelle fonction qui accepte les arguments restant. Si vous appelez une fonction curryfiée avec tous ses arguments, elle ne fera qu'appeler la fonction originale (NDT: ? it will just call the function).
Vous pouvez considérer une fonction curryfiée comme le meilleur des deux mondes: vous pouvez l'appeler normalement avec tous ses arguments, et cela marchera tout simplement. Ou bien vous pouvez l'appeler avec un sous-ensemble de ses arguments, et elle agira comme si vous aviez utilisé partial
.
Remarquez que cette souplesse apporte une petite baisse de performance, parce que curry
doit déterminer comment la fonction a été appelée et ce qu'elle doit faire. En général, je ne curry
des fonctions que quand j'ai besoin d'utiliser partial
plus d'une fois.
Tirons avantage de curry
pour notre fonction publishedInYear
. Notez que curry
marche toujours comme quand vous utilisiez partial
: il n'y a pas de version partialRight
. Nous développerons plus tard mais, pour l'instant, nous allons inverser les paramètres de publishedInYear
pour que l'année soit le premier paramètre.
const publishedInYear = curry((year, book) => book.year === year)
const titlesForYear = (books, year) => {
const selected = filter(publishedInYear(year), books)
return map(book => book.title, selected)
}
Nous pouvons de nouveau appeler publishedInYear
avec year
pour seul paramètre et recevoir une fonction qui prend un book
et exécute notre fonction originale. Cependant, nous pouvons encore l'appeler normalement avec publishedInYear(2012, book)
sans l'ennuyeuse syntaxe )(
. Le meilleur des deux mondes!
Ordre des paramètres
Remarquez que pour que curry
marche pour nous, nous avons dû inverser l'ordre des paramètres. C'est très courant en programmation fonctionnelle, et c'est pourquoi toutes les fonctions Ramda sont écrites pour que les données soient passées en dernière position.
Les paramètres précédents peuvent être vus comme la configuration du traitement. Donc pour publishedInYear
, le paramètre year
est la configuration (quelle année cherchons-nous?) et le paramètre book
est la donnée (où cherchons-nous l'année?).
Nous en avons déjà vu des exemples avec les fonctions d'itération de collection. Elles prennent toute la collection en dernier argument car cela rend ce style de programmation plus facile.
Arguments dans le mauvais ordre
Que se passerait-il si nous avions laissé l'ordre original des arguments de publishedInYear
? Comment pourrions-nous encore tirer parti de sa nature curryfiée?
Ramda propose quelques options.
Flip
La première option est flip
. flip
prend une fonction à deux arguments ou plus et retourne une nouvelle fonction qui prend les mêmes arguments mais échange l'ordre des deux premiers arguments. C'est principalement utilisé pour les fonction à deux arguments, mais c'est plus général que ça.
En utilisant flip
, on peut revenir à l'ordre original des arguments pour publishedInYear
:
const publishedInYear = curry((book, year) => book.year === year)
const titlesForYear = (books, year) => {
const selected = filter(flip(publishedInYear)(year), books)
return map(book => book.title, selected)
}
Dans la plupart des cas, je préfère utiliser l'ordre des arguments le plus pratique, mais si vous avez besoin d'utiliser une fonction que vous ne contrôlez pas, flip
est une aide appréciable.
Espace réservé
L'option la plus générale est l'argument espace reservé (NDT: placeholder): __
.
Et si nous avions une fonction curryfiée à trois arguments, que nous voulions lui fournir les premier et dernier arguments, laissant le dernier pour plus tard? Nous pouvons utiliser l'espace reservé (placeholder) à la place de l'argument du milieu:
const threeArgs = curry((a, b, c) => { /* ... */ })
const middleArgumentLater = threeArgs('value for a', __, 'value for c')
Vous pouvez aussi utiliser l'espace reservé plus d'une fois lors d'un appel de fonction. Par exemple, si nous ne voulions fournir que l'argument du milieu.
const threeArgs = curry((a, b, c) => { /* ... */ })
const middleArgumentOnly = threeArgs(__, 'value for b', __)
Nous pouvons utiliser l'espace réservé au lieu de flip
si nous voulons:
const publishedInYear = curry((book, year) => book.year === year)
const titlesForYear = (books, year) => {
const selected = filter(publishedInYear(__, year), books)
return map(book => book.title, selected)
}
Je trouve cette version plus lisible, mais si j'avais besoin de beaucoup utiliser la version flip
pée de publishedInYear
, je pourrais définir une fonction d'aide utilisant flip
, et j'utiliserais cette fonction d'aide partout. Nous verrons quelques exemples dans les chapitres suivants.
Remarquez que __
ne marche que pour les fonction curryfiées, alors que partial
, partialRight
et flip
fonctionnent sur n'importe quelle fonction. Si vous avez besoin d'utiliser __
sur une fonction normale, vous pouvez toujours l'encapsuler dans un appel à curry
.
Faisons un pipeline
Voyons si nous sommes capables de déplacer nos appels à filter
et map
dans un pipeline. Voici l'état du code, avec les arguments dans l'ordre que nous préférons:
const publishedInYear = curry((year, book) => book.year === year)
const titlesForYear = (books, year) => {
const selected = filter(publishedInYear(year), books)
return map(book => book.title, selected)
}
Dans le précédent chapitre, nous avons appris à utiliser pipe
et compose
, mais il nous manque une information pour pouvoir tirer parti de cet apprentissage.
Voici l'information manquante: presque toutes les fonctions Ramda sont curryfiées par défaut. Cela inclut filter
et map
. Donc filter(publishedInYear(year))
est parfaitement légitime et renvoie une nouvelle fonction qui ne fait qu'attendre que nous lui passions les livres, tout comme map(book => book.title)
.
Et maintenant nous pouvons écrire le pipeline:
const publishedInYear = curry((year, book) => book.year === year)
const titlesForYear = (books, year) =>
pipe(
filter(publishedInYear(year)),
map(book => book.title)
)(books)
Allons un cran plus loin et inversons les arguments de titlesForYear
pour nous conformer aux conventions de Ramda sur les données à la fin. Nous pouvons aussi curryfier la fonction pour permettre son utilisation dans des pipelines.
const publishedInYear = curry((year, book) => book.year === year)
const titlesForYear = curry((year, books) =>
pipe(
filter(publishedInYear(year)),
map(book => book.title)
)(books)
)
Conclusion
Ce chapitre est probablement le plus profond de ce livre. L'application partielle et la curryfication peuvent prendre du temps et réclamer un effort pour les intégrer. Mais une fois que vous les avez comprises, elles vous apportent une très puissante façon de transformer vos données d'un manière fonctionnelle.
Elles vous conduisent à commencer à construire des transformations en créant des pipelines de petits blocs simples.
Chapitre suivant
Pour écrire du code dans un style fonctionnel, nous devons commencer à penser «déclarativement». Pour ce faire, nous allons devoir trouver des moyens d'exprimer les constructions impératives dont nous avons l'habitude d'une manière fonctionnelle. Ces idées sont le sujet de Programmation déclarative.