POO vs Fonctionnel — 02 — Les piliers du fonctionnel : pureté, immutabilité, composition

Fonctions pures, immutabilité, composition, higher-order functions, currying et functors expliqués avec des exemples TypeScript et Python complets.

02 — Les piliers du fonctionnel : pureté, immutabilité, composition

Ce que tu vas apprendre

  • Ce qu'est une fonction pure et pourquoi ça change tout pour les tests
  • L'immutabilité : pas d'état qui change, des transformations de données
  • La composition de fonctions : construire le complexe à partir du simple
  • Les higher-order functions : map, filter, reduce et leurs équivalents
  • Le currying et l'application partielle
  • Exemples TypeScript et Python pour chaque concept

Prérequis


Pilier 1 : Les fonctions pures

Une fonction pure a deux propriétés :

  1. Déterministe : le même input produit toujours le même output
  2. Sans effets de bord : elle ne modifie rien en dehors d'elle-même (pas d'accès à une variable globale, pas d'écriture en BDD, pas de console.log)
typescript// Fonction pure — déterministe, sans effet de bord
function add(a: number, b: number): number {
  return a + b;
}

// Fonction impure — résultat dépend d'un état externe
let discount = 0.1;
function applyDiscount(price: number): number {
  return price * (1 - discount); // Dépend de `discount` — imprévisible
}

// Fonction impure — effet de bord
function logAndAdd(a: number, b: number): number {
  console.log(`${a} + ${b}`); // Effet de bord
  return a + b;
}
python# Fonction pure
def add(a: int, b: int) -> int:
    return a + b

# Fonction impure — état global
TAX_RATE = 0.2

def calculate_price(base: float) -> float:
    return base * (1 + TAX_RATE)  # Dépend de TAX_RATE — imprévisible si modifié

# Fonction pure — le taux est passé en paramètre
def calculate_price_pure(base: float, tax_rate: float) -> float:
    return base * (1 + tax_rate)

Pourquoi les fonctions pures sont faciles à tester

typescript// Test trivial — pas de mock, pas de setup
describe("add", () => {
  it("additionne deux nombres", () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });
});

// Test difficile — applyDiscount dépend de `discount`
describe("applyDiscount", () => {
  beforeEach(() => { discount = 0.1; });  // Setup — état global à réinitialiser
  afterEach(() => { discount = 0.1; });   // Teardown
  it("applique une remise", () => {
    expect(applyDiscount(100)).toBe(90);
  });
});
Déterministe Sans effet de bord Testable sans mock
Même input → même output Ne modifie rien d'externe Setup minimal, aucun état global

Pilier 2 : L'immutabilité

L'immutabilité signifie que les données ne changent jamais après leur création. Pour "modifier" une valeur, on crée une nouvelle valeur.

typescript// Mutable — dangereux avec des références partagées
const user = { name: "Alice", email: "alice@exemple.fr", role: "user" };
user.role = "admin"; // Mutation — si `user` est partagé, tout le monde voit le changement

// Immutable — transformation explicite
const user = { name: "Alice", email: "alice@exemple.fr", role: "user" } as const;
const adminUser = { ...user, role: "admin" }; // Nouveau objet

console.log(user.role);      // "user" — inchangé
console.log(adminUser.role); // "admin"
pythonfrom dataclasses import dataclass, replace

@dataclass(frozen=True)
class User:
    name: str
    email: str
    role: str

user = User(name="Alice", email="alice@exemple.fr", role="user")
admin_user = replace(user, role="admin")  # Nouveau objet

print(user.role)        # "user" — inchangé
print(admin_user.role)  # "admin"

L'immutabilité et les collections

typescriptconst numbers = [1, 2, 3, 4, 5];

// Mutable — modifie le tableau original
numbers.push(6);    // [1, 2, 3, 4, 5, 6] — l'original est changé
numbers.splice(0, 1); // Supprime le premier élément

// Immutable — retourne un nouveau tableau
const withSix = [...numbers, 6];            // Nouveau tableau
const withoutFirst = numbers.slice(1);       // Nouveau tableau
const doubled = numbers.map(n => n * 2);     // Nouveau tableau
const evens = numbers.filter(n => n % 2 === 0); // Nouveau tableau
pythonnumbers = [1, 2, 3, 4, 5]

# Mutable
numbers.append(6)    # Modifie la liste originale

# Immutable avec tuples
numbers_tuple = (1, 2, 3, 4, 5)
with_six = (*numbers_tuple, 6)          # Nouveau tuple
doubled = tuple(n * 2 for n in numbers_tuple)
evens = tuple(n for n in numbers_tuple if n % 2 == 0)

# Ou avec des list comprehensions (retournent de nouvelles listes)
numbers_list = [1, 2, 3, 4, 5]
doubled = [n * 2 for n in numbers_list]  # Nouveau tableau — original inchangé

Pilier 3 : La composition de fonctions

La composition consiste à combiner des fonctions simples pour former des fonctions plus complexes. La sortie d'une fonction devient l'entrée de la suivante.

typescript// Fonctions simples et pures
const trim = (s: string): string => s.trim();
const toLowerCase = (s: string): string => s.toLowerCase();
const removeSpaces = (s: string): string => s.replace(/\s+/g, "-");
const addPrefix = (prefix: string) => (s: string): string => `${prefix}-${s}`;

// Composition manuelle
function toSlug(title: string): string {
  return addPrefix("article")(removeSpaces(toLowerCase(trim(title))));
}

// Avec un helper compose (de droite à gauche)
function compose<T>(...fns: Array<(x: T) => T>): (x: T) => T {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
}

const toSlug2 = compose(addPrefix("article"), removeSpaces, toLowerCase, trim);
console.log(toSlug2("  Mon Article de Blog  ")); // "article-mon-article-de-blog"
typescript// Ou avec pipe (de gauche à droite — plus lisible)
function pipe<T>(...fns: Array<(x: T) => T>): (x: T) => T {
  return (x) => fns.reduce((acc, fn) => fn(acc), x);
}

const toSlug3 = pipe(trim, toLowerCase, removeSpaces, addPrefix("article"));
console.log(toSlug3("  Mon Article de Blog  ")); // "article-mon-article-de-blog"
pythonfrom functools import reduce
from typing import Callable, TypeVar

T = TypeVar('T')

def pipe(*fns: Callable) -> Callable:
    """Applique les fonctions de gauche à droite."""
    def apply(x):
        return reduce(lambda acc, fn: fn(acc), fns, x)
    return apply

# Fonctions simples
trim = str.strip
to_lower = str.lower
remove_spaces = lambda s: s.replace(' ', '-')
add_prefix = lambda prefix: lambda s: f"{prefix}-{s}"

to_slug = pipe(trim, to_lower, remove_spaces, add_prefix("article"))
print(to_slug("  Mon Article de Blog  "))  # "article-mon-article-de-blog"

Pilier 4 : Les higher-order functions

Une higher-order function (HOF) est une fonction qui prend une fonction en paramètre ou retourne une fonction. C'est ce qui rend les langages fonctionnels expressifs.

map, filter, reduce

typescriptconst orders = [
  { id: 1, amount: 150, status: "delivered" },
  { id: 2, amount: 80, status: "pending" },
  { id: 3, amount: 200, status: "delivered" },
  { id: 4, amount: 45, status: "cancelled" },
];

// map — transformer chaque élément
const amounts = orders.map(o => o.amount);
// [150, 80, 200, 45]

// filter — garder les éléments qui satisfont un prédicat
const delivered = orders.filter(o => o.status === "delivered");
// [{ id: 1, ... }, { id: 3, ... }]

// reduce — agréger en une valeur
const totalDelivered = orders
  .filter(o => o.status === "delivered")
  .map(o => o.amount)
  .reduce((sum, amount) => sum + amount, 0);
// 350
pythonorders = [
    {"id": 1, "amount": 150, "status": "delivered"},
    {"id": 2, "amount": 80, "status": "pending"},
    {"id": 3, "amount": 200, "status": "delivered"},
    {"id": 4, "amount": 45, "status": "cancelled"},
]

from functools import reduce

amounts = list(map(lambda o: o["amount"], orders))

delivered = list(filter(lambda o: o["status"] == "delivered", orders))

total_delivered = reduce(
    lambda acc, o: acc + o["amount"],
    filter(lambda o: o["status"] == "delivered", orders),
    0
)
# 350

# Python idiomatique — list comprehensions
amounts = [o["amount"] for o in orders]
delivered = [o for o in orders if o["status"] == "delivered"]
total_delivered = sum(o["amount"] for o in orders if o["status"] == "delivered")

Pilier 5 : Le currying et l'application partielle

Le currying transforme une fonction à plusieurs arguments en une séquence de fonctions à un argument. L'application partielle pré-remplit certains arguments.

typescript// Fonction classique
function multiply(a: number, b: number): number {
  return a * b;
}

// Fonction curryfiée
const curriedMultiply = (a: number) => (b: number): number => a * b;

const double = curriedMultiply(2);   // Application partielle — b est libre
const triple = curriedMultiply(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

// Cas d'usage réel : filtres réutilisables
const isStatus = (status: string) => (order: Order) => order.status === status;

const isDelivered = isStatus("delivered");
const isPending = isStatus("pending");

const delivered = orders.filter(isDelivered);
const pending = orders.filter(isPending);
pythonfrom functools import partial

def multiply(a: int, b: int) -> int:
    return a * b

double = partial(multiply, 2)   # Application partielle
triple = partial(multiply, 3)

print(double(5))  # 10
print(triple(5))  # 15

# Filtres réutilisables
def has_status(status: str, order: dict) -> bool:
    return order["status"] == status

is_delivered = partial(has_status, "delivered")
is_pending = partial(has_status, "pending")

delivered = list(filter(is_delivered, orders))
pending = list(filter(is_pending, orders))

Foncteurs et la gestion des effets

Un foncteur est un conteneur sur lequel on peut appliquer map sans sortir du conteneur.

typescript// Array est un foncteur
[1, 2, 3].map(x => x * 2) // [2, 4, 6] — toujours un Array

// Promise est un foncteur-like (then)
fetch("/api/user")
  .then(res => res.json())
  .then(user => user.name)

// Option / Maybe — foncteur pour les valeurs nullables
type Option<T> = { some: true; value: T } | { some: false };

function map<T, U>(opt: Option<T>, fn: (x: T) => U): Option<U> {
  return opt.some ? { some: true, value: fn(opt.value) } : { some: false };
}

const maybeUser: Option<{ name: string }> = { some: true, value: { name: "Alice" } };
const maybeName = map(maybeUser, u => u.name);
// { some: true, value: "Alice" }

const noUser: Option<{ name: string }> = { some: false };
const noName = map(noUser, u => u.name);
// { some: false } — pas d'erreur, pas de null

Python avec Optional :

pythonfrom typing import Optional, Callable, TypeVar

T = TypeVar('T')
U = TypeVar('U')

def fmap(value: Optional[T], fn: Callable[[T], U]) -> Optional[U]:
    """map sur Optional — foncteur."""
    return fn(value) if value is not None else None

user = {"name": "Alice"}
name = fmap(user, lambda u: u["name"])  # "Alice"

no_user = None
no_name = fmap(no_user, lambda u: u["name"])  # None — pas d'erreur

Ce que le fonctionnel résout bien (et ce qu'il ne résout pas)

Forces Limites
Tests triviaux — pas d'état, pas de mock Courbe d'apprentissage pour les concepts avancés (monades, functors)
Concurrence sûre — pas d'état partagé mutable Modélisation de domaines à état complexe (workflows, cycle de vie)
Pipelines de transformation lisibles Performances : créer de nouveaux objets à chaque transformation
Composition — réutilisation sans héritage Interopérabilité avec des librairies impératives

Résumé

Concept Définition Bénéfice clé
Fonction pure Déterministe + sans effet de bord Testabilité maximale
Immutabilité Pas de mutation — nouvelles valeurs Pas d'état partagé corrompu
Composition Chaîner des fonctions simples Réutilisabilité sans héritage
Higher-order functions map, filter, reduce, currying Abstraction concise
Foncteurs Conteneurs mappables Gestion des effets (null, erreurs)

Étape suivante : 03 — Comparaison directe — le même problème résolu avec les deux paradigmes côte à côte.


Sources

  • Hughes, J. (1990). Why Functional Programming Matters. The Computer Journal, 32(2), 98-107.
  • Abelson, H., & Sussman, G. J. (1996). Structure and Interpretation of Computer Programs (2nd ed.). MIT Press.
  • Wadler, P. (1992). The Essence of Functional Programming. Proceedings of POPL 1992.
  • Simpson, K. (2017). Functional-Light JavaScript. Leanpub.
  • Bird, R., & Wadler, P. (1988). Introduction to Functional Programming. Prentice Hall.
  • Chiusano, P., & Bjarnason, R. (2014). Functional Programming in Scala. Manning.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.