Skip to content

Programmation fonctionnelle

La programmation fonctionnelle s'articule autour de ces concepts : fonctions pures, récursivité, transparence référentielle, variables immuables, fonctions en tant que citoyens de première classe et fonctions d'ordre supérieur.

Programmation fonctionnelle pure

Les langages fonctionnels qualifiés de pures comme haskell n'admettent que la programmation fonctionnelle.

Immutabilité

  • L'immutabilité signifient qu'on ne peut pas changer la valeur d'une variable ou ses propriétés une fois qu'elle a été créée.
  • Pour changer de valeur, nous devons créer une copie tout en renseignant les nouvelles valeur.
  • Avec les objets, on classifie l'immutabilité en deux catégories:
    • Immutabilité profonde: On ne peut pas modifier un objet ou ses membres quelque soit leurs niveaux de hiérarchie ou leurs profondeurs o.a.b.c.... Dans le cas d'une collection, on ne peut plus lui ajouter ou supprimer d'éléments.
    • Immutabilité peu profonde: On ne peut pas modifier la variable en elle même mais on peut modifier ses membres si c'est un objet ou ajouter et supprimer des éléments si c'est une collection.
  • Avec les tableaux, on a deux types d'immutabilité:
    • Tableau immutable: On ne peut ni ajouter ou supprimer des éléments, ni changer la valeur des éléments existants.
    • Tableau en lecture seule: On ne peut ni ajouter ou supprimer des éléments mais on peut changer la valeur des éléments existants.
  • L'immutabilité profonde est la meilleure forme d'immutabilité.
    • En TypeScript, on peut le satisfaire en déclarant systématiquement en readonly toutes les propriétés et les tableaux.
    • En Rust, tout est immutable par défaut sauf si on utilise le qualificateur mut.
  • La modification d'un objet immutable se fait sur une copie profonde. Cette dernière peut être réalisée en une seule ligne ou à la main (en copiant les champs un par un), selon le langage utilisé.

Limite avec les fonctions de clonage profond

Les fonctions de clonage profond qui ne permettent pas de spécifier les propriétés à modifier ne sont pas utiles car elles génèrent une copie conforme à l'original qu'on ne pourra plus modifier.

Immutabilité
const items: readonly number[] = [1, 2];

// compile error
//items[0] = 2;

class Sauce {
  constructor(
    readonly mainIngredient: string,
    readonly secondaryIngredient: string
  ) {}
}

class Kebab {
  constructor(
    readonly name: string,
    readonly sauce: Sauce,
    readonly ingredients: readonly string[]
  ) {}
}

const k1 = new Kebab("miam", new Sauce("tomate", "huile"), ["Salade, Oignons"]);
console.log(k1);
// deep copy, but useless here because we can't touch k2
const k2 = structuredClone(k1);
console.log(k2);

// Manual deep copy
const k3 = new Kebab(k1.name, k1.sauce, ["Olives"]);
console.log(k3);

Fonctions pures et transparence référentielle

  • Les fonctions pures sont des fonctions qui n'ont pas d'effets secondaires et renverront donc toujours la même sortie étant donné la même entrée.
  • Transparence référentielle : signifie qu'une expression peut être remplacée par son résultat sans modifier le comportement du programme.
    • 💡 La transparence fait référence au fait que implémentation de l'expression n'est pas pertinente.
/**
 * A pure function return the same output for the same input and does have side-effects
 */
function add(a: number, b: number) {
  return a + b;
}

/**
 * This function is impure because it does not always return the same output given the same input
 */
function addImpure1(a: number, b: number) {
  return a + b + Math.random();
}

/**
 * This function is impure because it has a side effect (write to the console)
 */
function addImpure2(a: number, b: number) {
  console.log(a, b);
  return a + b;
}

Les fonctions comme citoyens de première classe

  • Les fonctions sont des citoyens de première classe : elles peuvent être affectées à une variable ou utilisées dans des fonctions d'ordre supérieur (passées en tant que paramètre de fonction à une autre fonction ou renvoyées par une fonction).
  • La plupart des langages permettent d'assigner de façon plus concise une fonction à une variable ou un argument.
    • C'est syntaxe a plusieurs noms, le plus commun est fonction lambda.
    • En TypeScript, on l'appelle fonction flèche
function add(a: number, b: number) {
  return a + b;
}

// a variable can point to a function
const f = add;
// As any usual variable, functions have a type (arg1: type1, ...) => return type
const g: (x: number, y: number) => number = add;
// A function used without () is not called and point to the location of its code
console.log("Function reference", f, add);
// A function reference can be called as any function
console.log("Function call", f(1, 2), add(2, -10));

// Arrow functions allow to assign a function to a vartiable or argument more quickly
// They are also called lambdas in other languages
const h = (x: number, y: number) => {
  return x * y;
};

/**
 * Arrow functions can be written in a single line
 * @param x
 * @param y
 * @returns the result of the expression x / y
 */
const p = (x: number, y: number) => x / y;

/**
 * compte is called a higher order function because
 * @param a left operand
 * @param b right operand
 * @param f a function that will be called with a and b as arguments
 */
function compute(a: number, b: number, f: (x: number, y: number) => number) {
  const result = f(a, b);
  console.log("f(a, b)", result);
}

console.log("compute call 1");
compute(5, 3, add);
console.log("compute call 2");
compute(5, 3, h);

// arrow functions allow to write more concise code in this case
compute(5, 3, (x, y) => x - y);

Programmation déclarative

  • La programmation usuelle est appeplée programmation impérative et résout le problème sous forme d'une suite d'instructions qui décrivent comment le programme doit se comporter étape par étape.
    • La boucle for est un exemple souvent utilisé pour illustrer la programmation impérative.
  • La programmation fonctionnelle décrit le résultat sous d'un enchainements de fonctions.
  • Permet d'exprimer ce que l'on veut obtenir mais pas comment l'obtenir.
  • Les fonctions les plus connues sont: filter, map et reduce.
const words = ["I", "love", "TypeScript", "in", "2022"];

// Count the total number of letters for all words that contain the letter 'i'

let totalImperative = 0;
for (const word of words) {
  if (word.includes("i")) {
    totalImperative += word.length;
  }
}
console.log("Imperative style: ", totalImperative);

const totalDeclarative = words
  .filter((w) => w.includes("i"))
  .map((w) => w.length)
  .reduce((acc, cur) => acc + cur, 0);

console.log("Declarative style:", totalDeclarative);

Exercices

  • Implémenter une fonction qui génère un tableau d'entiers aléatoires.
  • En utilisant un style déclaratif (filter, map, reduce), calculer:
    • La somme des éléments de la liste
    • La somme du double de chaque élément
    • Le produit des exponentielles des éléments pairs
    • La plus grande valeur inférieur à la moyenne
  • Créer une classe BinaryCalculator qui prend une fonction en argument du constructeur. Cette fonction prend deux nombres en entrée et retourne un nombre.
    • Définir la méthode run qui prend deux nombres en argument qu'on appellera a et b et exécute la méthode passée dans le constructeur en lui passant a et b. Si cette dernière retourne une valeur supérieure à 10, la méthode run affiche un message de succès, sinon la méthode run affiche un message d'erreur.
    • Définir la méthode runWithCallbacks qui prend deux nombres en argument qu'on appellera a et b ainsi que deux fonctions qu'on appellera success et failure respectivement.
      • runWithCallbacks exécute la méthode passée dans le constructeur. Si cette dernière retourne une valeur supérieure à 10, success est appelée en lui passant la valeur calculée, sinon failure est appelée en lui passant un message d'erreur.
      • On vous laisse le soin de définir le type de success et failure selon le besoin exprimé plus haut.
  • Instancier la classe BinaryCalculator pour faire de l'addition. Appeler les méthodes run et runWithCallbacks pour les arguments 5 et 1. Appeler les méthodes run et runWithCallbacks plusieurs fois pour faire de l'addition de 5 et un nombre aléatoire entre 0 et 10.
  • Instancier la classe BinaryCalculator pour calcule le minimum. Appeler les méthodes run et runWithCallbacks pour les arguments 5 et 20 puis pour 15 et 20.
Solution
function randomIntFromInterval(min: number, max: number): number {
  // min and max included
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function createRandomNumbers(): number[] {
  return [...Array(10).keys()].map(() => randomIntFromInterval(0, 20));
}

const numbers = createRandomNumbers();
console.log(numbers);

const sum = numbers.reduce((acc, cur) => acc + cur, 0);
const sumOfDouble = numbers
  .map((x) => x * 2)
  .reduce((acc, cur) => acc + cur, 0);
const sumOfDouble2 = numbers.reduce((acc, cur) => acc + cur * 2, 0);
const productOfExponentialEven = numbers
  .filter((x) => x % 2 === 0)
  .map((x) => Math.exp(x))
  .reduce((acc, cur) => acc * cur, 1);
const avg = sum / numbers.length;
const beforeAvg = numbers
  .filter((x) => x < avg)
  .reduce((acc, cur) => Math.max(acc, cur), -Infinity);
const beforeAvg2 = numbers
  .filter((x) => x < avg)
  .reduce((acc, cur) => (acc > cur ? acc : cur), -Infinity);
console.log(
  "sum",
  sum,
  "sumOfDouble",
  sumOfDouble,
  "productOfExponentialEven",
  productOfExponentialEven,
  "avg",
  avg,
  "beforeAvg",
  beforeAvg,
  beforeAvg2
);

class BinaryCalculator {
  constructor(readonly f: (x: number, y: number) => number) {}
  run(a: number, b: number) {
    const r = this.f(a, b);
    if (r > 10) {
      console.log("succes", r);
    } else {
      console.log("failure", r);
    }
  }
  runWithCallbacks(
    a: number,
    b: number,
    success: (x: number) => void,
    failure?: (x: { code: number; message: string }) => void
  ) {
    const r = this.f(a, b);
    if (r > 10) {
      success(r);
    } else if (failure !== undefined) {
      failure({
        code: 1,
        message: "Aie aie aie",
      });
    }
  }
}

const adder = new BinaryCalculator((x, y) => x + y);
adder.run(10, 5);
adder.runWithCallbacks(
  0,
  5,
  (x) => console.log("yahoo ! ", x),
  (e) => console.error("code:", e.code, "message:", e.message)
);

adder.runWithCallbacks(10, 5, (x) => console.log("yahoo ! ", x));

const minimizer = new BinaryCalculator(Math.min);
minimizer.run(10, 5);
minimizer.runWithCallbacks(
  0,
  5,
  (x) => console.log("yahoo ! ", x),
  (e) => console.error("code:", e.code, "message:", e.message)
);

minimizer.runWithCallbacks(12, 15, (x) => console.log("yahoo ! ", x));

Sources