Sep

Cet article qui annonce un long mais passionnant chemin vers la programmation fonctionnelle traitera du sujet de manière générale.

C’est quoi ce nouveau paradigme à la mode ?

La programmation fonctionnelle n’est pas nouvelle, bien au contraire. Elle existait avant la programmation orientée objet (POO). Et si vous en entendez parler de plus en plus, c’est surtout car elle permet de résoudre les problèmes que la plupart des développeurs rencontrent avec la POO. Un des problèmes récurrents est la gestion des effets de bords liés à la gestion d’état d’un objet.

Prenons un exemple d’une classe en java :

public class Employee {

  String name;
  Date BirthDate;

  public Employee(String name, Date BirthDate) {
    this.name = name;
    this.BirthDate = BirthDate;
  }

  public Date getBirthDate() {
    return this.BirthDate ;
  }
}

Et regardons ce qu’il se passe lors de l’instanciation :

public static void main(String[] args) {

  Date d = new Date (86, 6, 13);

  Employee e = new Employee("Bill", d);
  System.out.println (e.getBirthDate());
  //Sun Jul 13 00:00:00 GMT+01:00 1986

  d.setYear (80);
  System.out.println (e.getBirthDate());
  //Sun Jul 13 00:00:00 GMT+01:00 1980
}

Dans un premier temps nous avons créé un objet Date où nous avons stocké la référence dans la variable locale d qui, à son tour, est utilisée pour instancier l’employé « Bill ».

Lorsque l’on modifie l’objet référencé par la variable d, la date de naissance de Bill change. C’est un exemple simple mais typique d’effet de bords en programmation orientée objet.

La programmation fonctionnelle décrit une manière d’organiser un programme autour d’une composition de fonctions et qui répond à des problématiques récentes comme l’accès concurrent sur un état muable ou bien encore la scalabilité.

Elle permet, entre autre, d’avoir du code plus lisible, réutilisable et facile à tester.

Plutôt que de vous perdre à travers des explications sans fin, je vous propose de rentrer dans le vif du sujet en commençant par les fonctions First Class.

First Class avec classe !

Une fonction first class est une fonction traitée comme n’importe quel autre type de donnée et qui n’a pas de comportement particulier. 

Elle peut : 

  • être stockée dans une variable ou dans une structure de données
  • être passée en argument
  • être retournée par une fonction

 

Un petit exemple, ici nous déclarons une fonction :

  function greeting(string) {
    console.log(string);
  }
  greeting('Hello');  // 'Hello

Que l’on stocke dans la variable salute :

  const salute = greeting;
  salute('Hey hey');  // Hey hey

ability prend en argument et retourne la fonction salute :

  function ability(fn) {
    return fn;
  }

  const talk = ability(salute);
  talk('Howdy ho');  // Howdy ho

Certes, ce ne sont pas les exemples les plus utiles, mais retenons que les first class vont nous permettre de nous simplifier la tâche dans l’écriture de nos fonctions. Ce pattern nous permet en effet d’écrire du code plus lisible, plus dynamique et plus concis.

Un autre intérêt non négligeable est d’éviter les effets de bords en ne modifiant pas un état en dehors de son environnement local.

High Order Function

C’est tout simplement une fonction qui prend en argument ou retourne une fonction. Si vous débutez en Javascript, vous en avez peut-être déjà utilisé sans le savoir (Map par exemple est une High Order function native qui crée un nouveau tableau à partir du résultat de l’appel d’une fonction fournie sur chaque élément du tableau appelant).

const someNumbers = [10, 20, 30];

  function getHalf(num) {
    return num / 2;
  }

  const getHalfs = someNumbers.map(num => num / 2);
  console.log(getHalfs);  // logs "[5, 10, 15]"

  const divideBy2 = someNumbers.map(getHalf);
  console.log(divideBy2);  // logs "[5, 10, 15]"

Avec l’exemple ci-dessus, on peut vite se rendre compte que nous avons limité les effets de bords. Si getHalf venait à changer, cela restera transparent pour le reste du code.

La pureté avec une fonction pure

Par définition, une fonction pure est une fonction qui étant donné la même entrée, retournera toujours la même sortie, sans effet de bords. Illustrons cela avec un exemple :

  // impure
  let minimum = 18;
  const checkAge = age => age >= minimum;

Dans l’exemple impur, checkAge dépend de la variable minimum qui est hors de son scope et peut donc retourner un résultat différent. 

Nous allons voir comment rendre cette fonction plus pure pour éviter les effets de bords :

  // pure
  const checkAge = (age) => {
    const minimum = 21;
    return age >= minimum;
  };

Dans sa version pure, la fonction checkAge devient indépendante. L’intérêt de la pureté est d’éviter de changer l’état de l’application ainsi que les variables en dehors de sa portée.

Un dernier exemple pour assimiler la différence au niveau des effets de bords :

  // pure
  const add = (x, y) => x + y;

  add(2, 2);  // 4

  add(2, 2);  // 4

Dans sa version pure, la méthode add nous retourne toujours le même résultat pour les mêmes entrées.

  // impure
  let someNumber = 5;
  const add = x => x + someNumber;

  add(3);  // 13

  someNumber = 2;
  add(3);  // 5

Dans sa version impure, la méthode add nous retourne un résultat en fonction d’une variable (someNumber) qui est en dehors de sa portabilité.

Maintenant que le concept de pureté est assimilé, nous pouvons aller encore plus loin avec la mise en cache du résultat.

La memoization est le principe qui repose sur la mise en cache des résultats d’une opération longue. Notre exemple ne sera pas un cas d’utilisation car nous n’utilisons pas la memoization pour des opérations simples comme un add, mais pour des opérations compliquées.

L’idée est que, si nous avons utilisé la fonction une fois avec une certaine entrée, nous pouvons conserver les résultats que nous pourrons renvoyer en cas d’appel à cette fonction avec les mêmes paramètres plutôt que de recalculer les valeurs.

Ci-dessous un exemple simple d’une fonction memoize :

  const memoize = (fn) => {
    const cache = {};
    return (...args) => {
      const stringifiedArgs = JSON.stringify(args);
      cache[stringifiedArgs] = cache[stringifiedArgs] || fn(...args);
      return cache[stringifiedArgs];
    };
  };

Nous pouvons désormais mettre en cache le produit d’une fonction pure complexe ou longue en termes de traitement. 

  const veryComplicatedCalcul = memoize(x => (x + x));

  veryComplicatedCalcul(10);  // 20 depuis le résultat de la fonction

  veryComplicatedCalcul(10);  // 20 depuis le cache

Lorsque nous appelons à nouveau la fonction veryComplicatedCalcul avec le même paramètre, c’est depuis le cache que nous obtenons le résultat, le calcul n’est pas effectué à nouveau.

Easy to test

La programmation fonctionnelle tend à rendre les tests unitaires plus simplistes par l’intermédiaire des fonctions pures. 

Puisque celles-ci ne doivent opérer que sur l’entrée qui leur est donnée et renvoyer la sortie de cette opération sans effet secondaire, cela nous facilite la tâche. Pas besoin de mocker un service complexe ou de se préoccuper de l’état du système par exemple. Il suffit de fournir l’entrée de la fonction et d’en affirmer la sortie.

Les principes de la programmation fonctionnelle vous sont maintenant acquis.

Nous pouvons donc commencer à approfondir le sujet via le prochain article qui traitera de curryfication, composition et foncteur.

Malek CHAOUCHE

Related Posts

Leave A Comment