Programmation fonctionnelle en Javascript

C'est pas que de la hype, c'est aussi fort élégant et robuste.

Même s'il se débrouille pas mal, Javascript n'est pas un langage véritablement fonctionnel. Pour les puristes, Elm ou Haskell font bien mieux le job. Mais grâce à ce paradigme de programmation nous allons pouvoir écrire un code plus élégant, facile à lire (et donc à maintenir). Voici quelques concepts de base résumés pour ce lancer en douceur.

Les principes fondamentaux

L'immutabilité

En programmation fonctionnelle, on évite de faire changer les variables. D'ailleurs on va arrêter d'utiliser le mot "let" (ou pire encore le "var") pour utiliser des constantes partout avec le mot "const". Si on a besoin de faire changer une variable, on en crée une nouvelle :

//pas légal car mutable
let nombre = 0
nombre = nombre + 5

//légal car immutable
const nombre = 0
const nouveauNombre = nombre + 5

Cela a pour effet de limiter les effets de bords et d'obtenir un code plus prédictible.

Avec les tableaux et les objets le mot "const" n'est pas suffisant car javascript stock la référence du tableau dans la constante. Mais du coup on peu modifier le tableau comme on veut, tant qu'on ne change pas la référence du tableau, JS est content. C'est dans des situations comme ceci qu'on voit que Javascript n'est pas un langage complètement fonctionnel.

On peu cependant améliorer les choses avec "Object.freeze()"

const animaux = ['biquette',"oiseau","singe"]
animaux.push("belette")
// Ca fonctionne ... damned !

const animaux = ['biquette',"oiseau","singe"]  

Object.freeze(animaux)
animaux.push("belette")
// javascript nous bloque. Ouf !

D'ailleurs comment fait on pour ajouter un élément à un tableau immutable ? Une solution bien pratique est d'utiliser le spread operator (...monObjet) :

const animaux = ['biquette',"oiseau","singe"]
Object.freeze(animaux)
const animaux2=[...animaux,"belette"]

Les fonctions pures

Une fonction pure est une fonction qui renvoi toujours le même résultats quand on lui envoi les mèmes paramètres. Cela parait un peu débile, mais pas tant que cela. En pratique cela signifie que les fonctions ne doivent jamais utiliser ou modifier des variables globales. Elles doivent être bien étanches pour éviter les effets de bords.

const animaux = ['biquette',"oiseau","singe"]

//fonction impure (vade retro satanas !)
function ajouteAnimal1(bestiole){
  animaux.push(bestiole)
}
//On modifie le tableau "animaux" global. 
ajouteAnimal1("belette")

console.log("liste des animaux", animaux)

//fonction pure
function ajouteAnimal2(animaux,bestiole){
  return [...animaux,bestiole]
}
//On ne modifie pas le tableau global, donc c'est pur
console.log("liste des animaux", ajouteAnimal2(animaux,"belette"))

les Fonction d’ordre supérieur (High Order Functions)

Ce sont des fonctions qui peuvent prendre en paramètre une fonction, et/ou renvoient une fonction. Mon cerveau a un peu paniqué en entendant ça et il m'a fallut plus de pratique que de réfléxion pour comprendre le truc.

const mange = predateur => proie => `${predateur} mange ${proie}`
const chatQuiMange=mange("chat")

console.log(mange("chat")("souris"))
console.log(chatQuiMange("oiseau"))
On aurait aussi pu écrire :

const mange=function(predateur){
  return function(proie){ttt
    return `${predateur} mange ${proie}`
  }
}

const chatQuiMange=mange("chat")

console.log(mange("chat")("souris"))
console.log(chatQuiMange("oiseau"))

On voit qu'on peut ainsi composer ses propres fonctions comme "chatQuiMange". C'est idéal pour le testing en plus d'être à mon goût très agréable à lire.

Le tournoi des super combattants en programmation fonctionnelle

La théorie ça va bien 5 minutes, mais rien ne vaut un exemple concret pour mettre en pratique les concepts vus précédemment. Nous allons aussi voir les fonctions ".map", ".filter", ".reduce", ainsi que les concepts de chaînage et de currying.

Imaginez un tournoi interdimensionel avec Ryu, Naruto et Sangoku. Chaque personnage possède un nom, un type de combat, des attaques physiques, des attaques magiques et une défense .On va les représenter sous forme d'objets et les stocker dans un tableau. Après ils vont faire ce qu'ils savent faire le mieux : se mettre sur la tronche.

//Définition des combattants

const naruto = {
  name:"Naruto",
  type:"Ninja",
  attackPhysic:100,
  attackMagic:200,
  defense:500
}

const ryu={
  name:"Ryu",
  type:"Karateka",
  attackPhysic:150,
  attackMagic:60,
  defense:200
}

const sangoku={
  name:"sangoku",
  type:"Sayen",
  attackPhysic:200,
  attackMagic:300,
  defense:300
}

// Tableau des combattants
const fighters=[naruto,ryu,sangoku]

Round 1 : .map et chainage

J'aimerai avoir la liste de tous les combattants présentés de cette manière :

"Liste des combattants : Naruto - Ninja, Ryu - Karateka, Sangoku - Sayen"

On peut réaliser l'opération avec un code impératif de cette façon :

//Version impérative
const fightersWithNameAndType=[]

fighters.forEach(fighter=>{
  fightersWithNameAndType.push(fighter.name+ " - "+fighter.type)
})

const namesOfFighters=fightersWithNameAndType.join(", ")
console.log(`Liste des combattants : ${namesOfFighters}`)

Ou on peu utiliser la programmation fonctionnelle :

//Version fonctionnelle
const namesOfFighters=fighters
                      .map(fighter=>fighter.name+ " - "+fighter.type)
                      .join(", ")

console.log(`Liste des combattants : ${namesOfFighters}`)

Nettement plus classe non ? On va voir comment ça fonctionne.

Tout d'abord il y a l'utilisation de ".map". Il fonctionne comme le forEach sauf qu'il retourne le résultat du traitement sous forme de tableau :

NouveauTableau = tableau.map(elementTableau=>{ //fonction de traitement })

Ensuite avec le ".join" on fait ce qu'on appelle du chainage pour l'appliquer directement le résultat du ".map". Notre ".join" va ainsi prendre le résultat du map (un tableau du genre [(Nartuo - ninja),(Ryu - karateka)] pour transformer le tableau en chaine de caractères avec un "', " entre chaque valeur du tableau.

Round 2 : filter et point free style

Maintenant, j'aimerai avoir la liste des combattants offensifs, c'est à dire ceux qui ont plus de points d'attaque (magique + physique) que de défense. Le résultat devra être retourné sous la forme :

"Combattants offensifs : Ryu attaque : 210 défense :200, Sangoku attaque : 500 défense :300"

const getNameAndSkills= fighter =>fighter.name + " attaque : " 
                        + (fighter.attackMagic+fighter.attackPhysic)
                        + " défense :"+fighter.defense

const isAnAttacker = fighter => (fighter.attackMagic
                      +fighter.attackPhysic) > fighter.defense

const combattantsOffensifs = fighters
              .filter(isAnAttacker)
              .map(getNameAndSkills)
              .join(", ")

console.log(`Combattants offensifs : ${combattantsOffensifs}`)

On va décortiquer ce code tranquillement car c'est assez étrange à lire quand on est pas habitué à la programmation fonctionnelle.
on voit tout d'abord l'utilisation de ".filter". Il marche de cette façon :

NouveauTableau = tableau.filter(elementTableau => {//fonction du tableau à vérifier})

J'aurai pu écrire :

const combattantsOffensifs = 
      fighters.filter(fighter => 
                      (fighter.attackMagic+fighter.attackPhysic)
                      > fighter.defense)

Mais j'ai choisit de créer une fonction dédiée pour faire filtre que j'aurai pu écrire :

const isAnAttacker = fighter => (fighter.attackMagic
                      +fighter.attackPhysic) > fighter.defense

const combattantsOffensifs = fighters.filter(fighter=>isAnAttacker(fighter))

On peut encore simplifier le truc en utilisant le "point free style" qui permet de s'affranchir de l'envoi de paramètre un peu lourdingue : "fighter=>isAnAttacker(fighter)". On peu utiliser à la place juste "isAnAttacker" :

const isAnAttacker = fighter => (fighter.attackMagic
                      +fighter.attackPhysic) > fighter.defense

const combattantsOffensifs = fighters.filter(isAnAttacker)

C'est le même principe quand j'écris ".map(getNameAndSkills)" :

const combattantsOffensifs = fighters.map(getNameAndSkills)

on peut aussi écrire la même chose en moins élégant :

const combattantsOffensifs = fighters.map(fighter=> getNameAndSkills(fighter))

Round 3 : reduce et currying

Maintenant j'aimerai connaître la somme des attaques de tous les combattants. Pour cela je vais utiliser "reduce", qui va et bien ... hum... réduire un tableau. concrètement, il va ajouter chaque valeur du tableau à un accumulateur et retourner le résultat.

const tableauReduit = tableau.reduce(
        (accumulateur,valeurCourrante)=>accumulateur + valeurCourrante,
        valeurDeDepart
      )

Pour connaître la puissance d'attaque de tous les combattants on peu le faire de cette façon :

const attackTotal = fighters.reduce(
    (acc, fighter) => acc + fighter.attackPhysic + fighter.attackMagic,
    0
  )

console.log("Attaque totale de tous les combattants : " + attackTotal)

Maintenant j'aimerai savoir si tous les héros peuvent détruire la terre s'ils l'attaquent ensemble dans une attaque commune. Je vais comparer les points de défense de la terre à la somme de toutes les attaques des combattants. Voici le code que je vais décortiquer ensuite :

//Calcule la force d'attaque des combattants
const attackTotal = fighters.reduce(
    (acc, fighter) => acc + fighter.attackPhysic + fighter.attackMagic,
    0
  )
console.log("Attaque totale de tous les combattants : " + attackTotal)

const earthDefense=2000

//Calcule si la terre est en danger
const canTheyDestroyEarth=earthDefense=>attackTotal=>attackTotal >= earthDefense

//Affiche le résultat
if(canTheyDestroyEarth(earthDefense)(attackTotal)){
    console.log("Ils peuvent détruire la terre")
}else{
    console.log("Ils ne peuvent pas détruire la terre (ouf!)")
}

On voit ici une fonction aux paramètres bien étranges :

const canTheyDestroyEarth=earthDefense=>attackTotal=>attackTotal > earthDefense
En fait cette fonction est la forme réduite de :

//Code version verbeux 
const canTheyDestroyEarth=function(earthDefense){
  return function(attackTotal){
   return attackTotal > earthDefense
  }
}

//transformation du code V2
const canTheyDestroyEarthV2=(earthDefense)=>{
  return (attackTotal)=>{
    //si la fonction ne fait que retourner une valeur, pas besoin d'indiquer le return
    attackTotal > earthDefense
  }
}

//transformation du code V3
const canTheyDestroyEarthV3=earthDefense=>attackTotal=>attackTotal > earthDefense

Dans cette forme verbeuse du code on voit l'utilisation du currying. Le currying est le fait de passer des X arguments à X fonctions contenant prenant chacune un seul paramêtre. La combinaison d'appel de fonction dans une fonction est possible car les variables des fonctions parents sont accessibles dans les fonctions enfants. C'est aussi ce qui fait tout l'intéret du concept : les variables sont encapsulées et isolées dans chaque fonction, ce qui limite les problèmes d'état de notre application.

Pour appeler une fonction avec du currying on utilise la forme :

canTheyDestroyEarth(earthDefense)(attackTotal)
//Envoi de earthDefense à la première fonction
//Envoi de attackTotal à la deuxième fonction
et non la formule habituelle :

canTheyDestroyEarth(earthDefense, attackTotal)
//Envoi de earthDefense et attackTotal à une seule et même fonction

goku

J'espère que cette mise en bouche vous a donné envie de vous y mettre. N'hésitez pas à me contacter si certains passages paraissent obscurs pour que j'améliore cet article.