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,reduceet 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 :
- Déterministe : le même input produit toujours le même output
- 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.