DEV Community

Cover image for La programmation purement fonctionnelle
DURAND Malo
DURAND Malo

Posted on

La programmation purement fonctionnelle

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;
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }; }
Enter fullscreen mode Exit fullscreen mode

Une deuxième fonction qui isole le champs head pour obtenir la valeur :

const head = (pair) => pair.head;
Enter fullscreen mode Exit fullscreen mode

Et une troisième fonction qui isole le champs tail pour obtenir le reste de la paire :

const tail = (pair) => pair.tail;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 ]
Enter fullscreen mode Exit fullscreen mode
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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Affichera cette paire :

{
    head: 2,
    tail: {
        head: 1,
        tail: {
            head: 0,
            tail: null
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
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)));
Enter fullscreen mode Exit fullscreen mode

Ainsi, le code :

console.log(list2array(map((x) => x + x)(range(0)(3))));
Enter fullscreen mode Exit fullscreen mode

Produira la sortie suivante :

{
    head: 4,
    tail: {
        head: 2,
        tail: {
            head: 0,
            tail: null
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 obtiendrez Fizz,
  • passez un nombre divisible par 5 à la fonction Fizzbuzz, vous obtiendrez Buzz,
  • passez un nombre divisible par 3 et 5 à la fonction Fizzbuzz, vous obtiendrez FizzBuzz,
  • 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;
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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'
]
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Top comments (0)