Bonjour à vous. Aujourd'hui, on s'intéresse à la programmation purement fonctionnelle du point de vue d'un développeur utilisant la programmation impérative, en JavaScript.
Introduction :
La programmation purement fonctionnelle est un paradigme de programmation déclarative dans lequel un programme est construit autour d'appels de fonctions et de compositions de fonctions. Il n'y a pas d'exécution séquentielle d'instruction, mais plutôt des appels consécutifs à des fonctions, elles-mêmes appelant d'autres fonctions, etc...
Dans ce paradigme, les fonctions peuvent être passées en paramètres, mais également devenir une valeur de retour, comme un type de donnée.
Le but étant d'avoir de toutes petites fonctions avec une seule tâche spécifique qui s'imbriquent avec d'autres fonctions, permettant au fur et à mesure de complexifier un programme.
Nous allons observer la programmation purement fonctionnelle avec ces quelques règles :
- fonction d'une ligne, constituée uniquement d'un
return
, - pas plus d'un paramètre par fonction,
- pas de boucle,
- pas de condition (sauf opérateurs ternaires),
- pas d'effet indésirable :
- pas de modification de valeur des paramètres,
- pas d'Input/Output,
- pas de variables.
- pas de tableau,
Tant de règles ?
Ça ne vous a probablement pas échappé, mais comment faire un programme qui tienne la route avec autant de restrictions ?
Fonction d'une ligne ? Pas plus d'un paramètre ?
Pour passer plusieurs paramètres à une fonction tout en respectant notre règle, il suffit de créer une fonction qui prend un paramètre et retourne une autre fonction, elle aussi prenant un paramètre.
Il s'agit d'une technique appelée Currying
. Vous renvoyez autant de fonctions que de paramètres souhaités, jusqu'à pouvoir renvoyer un résultat.
Par exemple, voici une fonction qui effectue une somme entre deux nombres :
// Programmation impérative :
const add = (a, b) => a + b;
// Programmation purement fonctionnelle :
const functionnalAdd = (a) => (b) => a + b;
Pas de boucle ? Pas de condition ?
Et non, vous n'avez pas besoin de boucle pour faire un programme. Si vous avez déjà programmé avant, ou même si vous avez suivi des cours de mathématiques, vous avez entendu parler respectivement de récursivité et de récurrence.
La récursivité consiste en une fonction qui s'appelle elle-même un nombre déterminé de fois, créant ainsi une boucle. Dans des langages comme Python, la récursivité connait sa limite à 1000 appels successifs. Dans d'autres langages, comme Haskell, cette limite n'existe pas et vous devrez vous-même interrompre un programme qui ne s'arrête pas.
Dans cet exemple de récursivité, nous allons coder une fonction appelée sumtorial
qui prend un paramètre n
correspondant à une limite telle que : sumtorial(n) = 0 + 1 + 2 + ... + n - 2 + n - 1 + n
// Programmation impérative :
const sumtorial = (n) => {
let result = 0;
for (let i = 0; i <= n; ++i)
result += i;
return result;
}
// Programmation purement fonctionnelle :
const functionnalSumtorial = (n) => n === 0 ? 0 : n + functionnalSumtorial(n - 1);
Pas de tableau ?
Et oui, les tableaux ne sont pas admis ici. Nous allons à la place utiliser un système de paires. Une paire est similaire à une liste chaînée où les champs data
et next
correspondent respectivement aux champs head
et tail
. Vous pouvez la visualiser comme suit :
{
head: 30,
tail: {
head: 20,
tail: {
head: 10,
tail: null
}
}
}
Pour l'implémentation, nous aurons besoin de trois fonctions.
Une première fonction qui construit la paire :
const pair = (head) => (tail) => { return { head, tail }; }
Une deuxième fonction qui isole le champs head
pour obtenir la valeur :
const head = (pair) => pair.head;
Et une troisième fonction qui isole le champs tail
pour obtenir le reste de la paire :
const tail = (pair) => pair.tail;
Nous allons ensuite créer deux fonctions qui ne sont pas purement fonctionnelles. Il s'agit de deux fonctions qui transgressent nos règles uniquement à des fins de vérification. Elles sont à proscrire dans nos appels de fonctions.
// Converti un tableau en une liste chaînée "paire".
const array2list = (array) => {
let p = null;
for (let i = 0; i < array.length; ++i)
p = pair(array[i])(p);
return p;
}
// Converti une liste chaînée "paire" en un tableau
const list2array = (p) => {
const array = [];
while (p !== null) {
array.push(head(p));
p = tail(p);
}
array.reverse();
return array;
}
Pour vérifier qu'une paire est créée correctement, vous pouvez exécuter ce code :
const paire = array2list([10, 20, 30]);
const liste = list2array(paire);
console.log(liste);
// Sortie :
// [ 10, 20, 30 ]
Implémentation de la fonction range
La fonction range
prend deux paramètres. Un minimum et un maximum. Nous allons renvoyer une paire contenant tous les nombres entre le minimum inclu et le maximum exclu.
const range = (min) => (max) => max <= min ? null : pair(max - 1)(range(min)(max - 1));
Si le max est supérieur ou égal au min, on veut arrêter la récursivité car on a terminé l'iteration. Pour se faire, on renvoie null
.
Sinon, on renvoie une paire avec pour head
, max - 1
(puisqu'on exclu le maximum), et comme tail
, un appel récursif avec le même minimum, mais on soustrait 1
à max
pour diminuer la prochaine valeur de la paire.
Ainsi, le code :
console.log(range(0)(3));
Affichera cette paire :
{
head: 2,
tail: {
head: 1,
tail: {
head: 0,
tail: null
}
}
}
Implémentation de la fonction map
La fonction map
prend également deux paramètres. Le premier est une fonction, le deuxième est une paire.
Pour chaque élément de la paire, on appelle la fonction f
avec comme paramètre, la head
de la paire.
map
nous renverra une nouvelle paire avec les nouvelles valeurs.
const map = (f) => (p) => p === null ? null : pair(f(head(p)))(map(f)(tail(p)));
Ainsi, le code :
console.log(list2array(map((x) => x + x)(range(0)(3))));
Produira la sortie suivante :
{
head: 4,
tail: {
head: 2,
tail: {
head: 0,
tail: null
}
}
}
Et maintenant ?
Maintenant, le plus dur est derrière nous. Nous avons une petite boîte à outils remplie de fonctions purement fonctionnelles, et on compte bien s'en servir !
Nous allons résoudre le célèbre problème FizzBuzz
. Voyez le problème suivant :
- passez un nombre divisible par 3 à la fonction
Fizzbuzz
, vous obtiendrezFizz
, - passez un nombre divisible par 5 à la fonction
Fizzbuzz
, vous obtiendrezBuzz
, - passez un nombre divisible par 3 et 5 à la fonction
Fizzbuzz
, vous obtiendrezFizzBuzz
, - passez un nombre qui n'est ni divisible par 3 ni par 5 à la fonction
Fizzbuzz
, vous obtiendrez ce même nombre.
Nous allons passez 100 nombres à la fonction FizzBuzz
, allant de 1 à 100 inclu.
D'abord, FizzBuzz
Avant d'appliquer FizzBuzz
sur une liste de 100 éléments, il faut déjà pouvoir l'appliquer sur un seul.
Voici une définition très épurée :
const fizzbuzz = (n) => ((n % 3 === 0 ? "Fizz": "") + (n % 5 === 0 ? "Buzz": "")) || n;
Si n % 3 === 0
alors n
est divisible par 3 et on renvoie Fizz
. On renvoie une chaîne vide sinon.
Si n % 5 === 0
alors n
est divisible par 5 et on renvoie Buzz
. On renvoie une chaîne vide sinon.
Ensuite, on concatène les deux chaînes renvoyées par chacune des expressions. Les résultats possibles sont Fizz
, Buzz
, FizzBuzz
ou une chaîne vide.
Enfin, on utilise l'opérateur ||
pour détecter une chaîne vide. Si la chaîne est vide, le membre de droite sera alors la valeur de retour. En l'occurence, notre nombre.
Nous avons respecté les règles du FizzBuzz mais également de la programmation purement fonctionnelle.
FizzBuzz
Nous voilà !
Maintenant, nous pouvons appliquer FizzBuzz
sur une liste de 100 nombres très facilement. Comment ? Et bien on utilise la fonction range
pour avoir une liste de 1 à 100 inclu, on applique la fonction fizzbuzz
à chaque élément de la liste grâce à la fonction map
, et on affiche le résultat :
const solution = map(fizzbuzz)(range(1)(101));
console.log(list2array(solution));
Voilà l'affichage que vous devriez obtenir :
[
1, 2, 'Fizz', 4, 'Buzz', 'Fizz',
7, 8, 'Fizz', 'Buzz', 11, 'Fizz',
13, 14, 'FizzBuzz', 16, 17, 'Fizz',
19, 'Buzz', 'Fizz', 22, 23, 'Fizz',
'Buzz', 26, 'Fizz', 28, 29, 'FizzBuzz',
31, 32, 'Fizz', 34, 'Buzz', 'Fizz',
37, 38, 'Fizz', 'Buzz', 41, 'Fizz',
43, 44, 'FizzBuzz', 46, 47, 'Fizz',
49, 'Buzz', 'Fizz', 52, 53, 'Fizz',
'Buzz', 56, 'Fizz', 58, 59, 'FizzBuzz',
61, 62, 'Fizz', 64, 'Buzz', 'Fizz',
67, 68, 'Fizz', 'Buzz', 71, 'Fizz',
73, 74, 'FizzBuzz', 76, 77, 'Fizz',
79, 'Buzz', 'Fizz', 82, 83, 'Fizz',
'Buzz', 86, 'Fizz', 88, 89, 'FizzBuzz',
91, 92, 'Fizz', 94, 'Buzz', 'Fizz',
97, 98, 'Fizz', 'Buzz'
]
Conclusion :
La programmation fonctionnelle est un paradigme fascinant parce qu'il permet d'avoir des fonctions qui n'ont qu'une seule tâche, ce qui les rend simple à débogguer. Ce paradigme épure le code que vous auriez l'habitude de produire.
En dehors de ça, étant relativement peu exploité, il force le développeur à explorer de nouvelles façons de penser et de réfléchir à son code.
Si vous aviez peur de la programmation purement fonctionnelle, j'espère que vous aurez maintenant envie d'en apprendre plus.
Je remercie le streamer / YouTuber tsoding
qui ma transmit ce savoir. Il s'agit d'un russe anglophone que je suis depuis un moment et que je trouve très intelligent / pédagogue. Si vous parlez anglais, c'est une mine d'or d'informations. Voici sa chaîne YouTube :
https://www.youtube.com/tsoding
Voici le code complet :
/**
* Programmation purement fonctionnelle
*/
const pair = (head) => (tail) => { return { head, tail }; }
const head = (pair) => pair.head;
const tail = (pair) => pair.tail;
const range = (min) => (max) => max <= min ? null : pair(max - 1)(range(min)(max - 1));
const map = (f) => (p) => p === null ? null : pair(f(head(p)))(map(f)(tail(p)));
const fizzbuzz = (n) => ((n % 3 === 0 ? "Fizz": "") + (n % 5 === 0 ? "Buzz": "")) || n;
const solution = map(fizzbuzz)(range(1)(101));
/**
* Programmation non purement fonctionnelle
* Les règles peuvent être transgresser ici uniquement.
*/
const array2list = (array) => {
let p = null;
for (let i = 0; i < array.length; ++i)
p = pair(array[i])(p);
return p;
}
const list2array = (p) => {
const array = [];
while (p !== null) {
array.push(head(p));
p = tail(p);
}
array.reverse();
return array;
}
console.log(list2array(solution));
Top comments (0)