Chapitre 16
16. Patterns Fonctionnels
16. Patterns Fonctionnels
Patterns Fonctionnels - Cours Détaillé
Introduction à la Programmation Fonctionnelle
La programmation fonctionnelle (FP) est un paradigme où les programmes sont construits par composition de fonctions pures, sans état mutable ni effets de bord. Les patterns fonctionnels sont des solutions réutilisables à des problèmes récurrents dans ce paradigme.
Principes fondamentaux
- Immutabilité : Les données ne sont jamais modifiées après création
- Fonctions pures : Même entrée → même sortie, pas d'effets de bord
- Composition : Construire des fonctions complexes à partir de fonctions simples
- Typage fort : Les types sont explicites et vérifiés à la compilation
Pourquoi les patterns fonctionnels ?
- Prédictibilité : Les fonctions pures sont faciles à raisonner
- Testabilité : Pas besoin de mocks pour les fonctions pures
- Parallélisation : L'absence d'état mutable simplifie la concurrence
- Réutilisabilité : Les fonctions sont des citoyens de première classe
1. Functor
Définition
Un Functor est une structure de données qui implémente une fonction map. Cette fonction applique une transformation à la valeur contenue sans modifier la structure.
Loi du Functor
- Identité :
functor.map(x => x) === functor - Composition :
functor.map(f).map(g) === functor.map(x => g(f(x)))
Implémentation en TypeScript
interface Functor<T> {
map<U>(fn: (value: T) => U): Functor<U>;
}
// Functor Array (natif)
// [1, 2, 3].map(x => x * 2) → [2, 4, 6]
// Functor Option
class Option<T> implements Functor<T> {
private constructor(private value: T | null) {}
static some<T>(value: T): Option<T> {
return new Option(value);
}
static none<T>(): Option<T> {
return new Option<T>(null);
}
map<U>(fn: (value: T) => U): Option<U> {
if (this.value === null) return Option.none<U>();
return Option.some(fn(this.value));
}
isSome(): boolean {
return this.value !== null;
}
getOrElse(defaultValue: T): T {
return this.value !== null ? this.value : defaultValue;
}
}
// Utilisation
const result = Option.some(5)
.map(x => x * 2)
.map(x => x + 1);
// Option.some(11)
Functors courants
| Structure | map behavior |
|---|---|
| Array | Applique à chaque élément |
| Promise | Applique à la valeur résolue |
| Option | Applique si Some, skip si None |
| Either | Applique à Right, ignore Left |
| Observable | Applique à chaque émission |
2. Monade (Maybe, Either, IO)
Définition
Une Monade est une structure qui représente une computation comme une série d'étapes. Elle implémente :
of(oureturn) : Enveloppe une valeur dans la monadeflatMap(oubind,chain) : Enchaîne des computations
Loi de la Monade
- Left identity :
Monad.of(x).flatMap(f) === f(x) - Right identity :
monad.flatMap(Monad.of) === monad - Associativité :
monad.flatMap(f).flatMap(g) === monad.flatMap(x => f(x).flatMap(g))
Maybe Monad
class Maybe<T> {
private constructor(private value: T | null | undefined) {}
static of<T>(value: T | null | undefined): Maybe<T> {
return new Maybe(value);
}
static just<T>(value: T): Maybe<T> {
return new Maybe(value);
}
static nothing<T>(): Maybe<T> {
return new Maybe<T>(null);
}
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.value == null) return Maybe.nothing<U>();
return Maybe.just(fn(this.value));
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
if (this.value == null) return Maybe.nothing<U>();
return fn(this.value);
}
getOrElse(defaultValue: T): T {
return this.value != null ? this.value : defaultValue;
}
isNothing(): boolean {
return this.value == null;
}
}
// Utilisation
function getUser(id: number): Maybe<User> { /* ... */ }
function getAddress(user: User): Maybe<Address> { /* ... */ }
function getCity(address: Address): Maybe<string> { /* ... */ }
const city = getUser(1)
.flatMap(getAddress)
.flatMap(getCity)
.getOrElse('Unknown');
Either Monad
type Either<L, R> = Left<L, R> | Right<L, R>;
class Left<L, R> {
readonly value: L;
constructor(value: L) {
this.value = value;
}
isLeft(): this is Left<L, R> { return true; }
isRight(): this is Right<L, R> { return false; }
map<U>(fn: (value: R) => U): Either<L, U> {
return new Left<L, U>(this.value);
}
flatMap<U>(fn: (value: R) => Either<L, U>): Either<L, U> {
return new Left<L, U>(this.value);
}
getOrElse(defaultValue: R): R {
return defaultValue;
}
}
class Right<L, R> {
readonly value: R;
constructor(value: R) {
this.value = value;
}
isLeft(): this is Left<L, R> { return false; }
isRight(): this is Right<L, R> { return true; }
map<U>(fn: (value: R) => U): Either<L, U> {
return new Right<L, U>(fn(this.value));
}
flatMap<U>(fn: (value: R) => Either<L, U>): Either<L, U> {
return fn(this.value);
}
getOrElse(defaultValue: R): R {
return this.value;
}
}
// Helpers
function left<L, R>(value: L): Either<L, R> {
return new Left(value);
}
function right<L, R>(value: R): Either<L, R> {
return new Right(value);
}
// Utilisation
function divide(a: number, b: number): Either<string, number> {
if (b === 0) return left('Division by zero');
return right(a / b);
}
function parseJSON(json: string): Either<string, unknown> {
try {
return right(JSON.parse(json));
} catch (e) {
return left(`Parse error: ${e.message}`);
}
}
const result = right(10)
.flatMap(x => divide(x, 2))
.flatMap(x => right(x * 3))
.getOrElse(0);
// 15
IO Monad
class IO<T> {
constructor(private effect: () => T) {}
static of<T>(value: T): IO<T> {
return new IO(() => value);
}
map<U>(fn: (value: T) => U): IO<U> {
return new IO(() => fn(this.effect()));
}
flatMap<U>(fn: (value: T) => IO<U>): IO<U> {
return new IO(() => fn(this.effect()).run());
}
run(): T {
return this.effect();
}
}
// Utilisation
function readFile(path: string): IO<string> {
return new IO(() => fs.readFileSync(path, 'utf-8'));
}
function writeFile(path: string, content: string): IO<void> {
return new IO(() => fs.writeFileSync(path, content));
}
const program = readFile('input.txt')
.map(content => content.toUpperCase())
.flatMap(content => writeFile('output.txt', content));
program.run(); // Effet de bord uniquement ici
3. Currying
Définition
Le currying transforme une fonction de n arguments en une chaîne de fonctions unaires.
Principe
// Sans currying
const add = (a: number, b: number, c: number) => a + b + c;
// Avec currying
const curriedAdd = (a: number) => (b: number) => (c: number) => a + b + c;
curriedAdd(1)(2)(3); // 6
Implémentation générique
type Curry<P extends any[], R> =
P extends [infer H, ...infer T]
? (arg: H) => Curry<T, R>
: R;
function curry<P extends any[], R>(fn: (...args: P) => R): Curry<P, R> {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs: any[]) => curried(...args, ...nextArgs);
} as any;
}
// Utilisation
const curriedSum = curry((a: number, b: number, c: number) => a + b + c);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2, 3); // 6
Intérêt du currying
// Création de fonctions spécialisées
const multiply = (a: number) => (b: number) => a * b;
const double = multiply(2);
const triple = multiply(3);
double(5); // 10
triple(5); // 15
// En combinaison avec map
[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]
4. Partial Application
Définition
La partial application fixe un nombre d'arguments d'une fonction, produisant une fonction avec moins d'arguments.
Différence avec le currying
- Currying : Transforme en fonctions unaires chaînées
- Partial Application : Fixe certains arguments, quel que soit leur nombre
function partial<T, R>(fn: (...args: T[]) => R, ...presetArgs: T[]) {
return (...laterArgs: T[]) => fn(...presetArgs, ...laterArgs);
}
// Utilisation
function greet(greeting: string, name: string, punctuation: string) {
return `${greeting}, ${name}${punctuation}`;
}
const greetHello = partial(greet, 'Hello');
const greetHelloWorld = partial(greet, 'Hello', 'World');
greetHello('Alice', '!'); // "Hello, Alice!"
greetHelloWorld('.'); // "Hello, World."
Partial avec placeholders
const _ = Symbol('placeholder');
function partialWithPlaceholders<T, R>(
fn: (...args: T[]) => R,
...presetArgs: (T | symbol)[]
) {
return (...laterArgs: T[]) => {
const args = presetArgs.map(arg =>
arg === _ ? laterArgs.shift()! : arg
);
return fn(...args, ...laterArgs);
};
}
function formatUrl(protocol: string, domain: string, path: string) {
return `${protocol}://${domain}/${path}`;
}
const formatHttps = partialWithPlaceholders(formatUrl, 'https', _, _);
formatHttps('example.com', 'api/users'); // "https://example.com/api/users"
5. Composition de fonctions
Définition
La composition combine deux ou plusieurs fonctions pour en créer une nouvelle. compose(f, g)(x) = f(g(x)).
function compose<T, R>(...fns: Function[]): (...args: any[]) => R {
return (x: any) => fns.reduceRight((acc, fn) => fn(acc), x);
}
// Version typée
type Func<A, B> = (a: A) => B;
function compose2<A, B, C>(f: Func<B, C>, g: Func<A, B>): Func<A, C> {
return (x: A) => f(g(x));
}
// Utilisation
const trim = (s: string) => s.trim();
const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1);
const exclaim = (s: string) => s + '!';
const formatName = compose(exclaim, capitalize, trim);
formatName(' alice '); // "Alice!"
Composition à gauche (pipe)
function pipe<T, R>(...fns: Function[]): (...args: any[]) => R {
return (x: any) => fns.reduce((acc, fn) => fn(acc), x);
}
const processString = pipe(trim, capitalize, exclaim);
processString(' alice '); // "Alice!"
// En lecture naturelle : trim → capitalize → exclaim
6. Pipeline Pattern
Définition
Le pipeline pattern est une chaîne de transformations où la sortie de chaque étape est l'entrée de la suivante.
class Pipeline<T> {
private steps: Array<(value: T) => T> = [];
static of<T>(value: T): Pipeline<T> {
const pipeline = new Pipeline<T>();
return pipeline;
}
pipe(fn: (value: T) => T): Pipeline<T> {
this.steps.push(fn);
return this;
}
execute(initial: T): T {
return this.steps.reduce((value, fn) => fn(value), initial);
}
}
// Utilisation
interface UserData {
name: string;
email: string;
role: string;
}
const validate = (user: UserData) => {
if (!user.email.includes('@')) throw new Error('Invalid email');
return user;
};
const normalize = (user: UserData) => ({
...user,
email: user.email.toLowerCase(),
name: user.name.trim(),
});
const enrich = (user: UserData) => ({
...user,
createdAt: new Date(),
isActive: true,
});
const createUserPipeline = new Pipeline<UserData>()
.pipe(validate)
.pipe(normalize)
.pipe(enrich);
const user = createUserPipeline.execute({
name: ' Alice ',
email: 'Alice@Example.COM',
role: 'user',
});
Pipeline asynchrone
class AsyncPipeline<T> {
private steps: Array<(value: T) => Promise<T>> = [];
pipe(fn: (value: T) => Promise<T>): AsyncPipeline<T> {
this.steps.push(fn);
return this;
}
async execute(initial: T): Promise<T> {
let value = initial;
for (const step of this.steps) {
value = await step(value);
}
return value;
}
}
// Utilisation
const fetchUser = async (id: number): Promise<User> => api.getUser(id);
const enrichWithOrders = async (user: User): Promise<User> => {
const orders = await api.getOrders(user.id);
return { ...user, orders };
};
const cacheUser = async (user: User): Promise<User> => {
await cache.set(`user:${user.id}`, user);
return user;
};
const pipeline = new AsyncPipeline<User>()
.pipe(enrichWithOrders)
.pipe(cacheUser);
const result = await pipeline.execute(await fetchUser(1));
7. Immutabilité
Définition
L'immuabilité signifie qu'une fois créée, une donnée ne peut pas être modifiée. Toute "modification" crée une nouvelle instance.
Implémentation en TypeScript
// readonly en TypeScript
interface User {
readonly id: number;
readonly name: string;
readonly address: Readonly<Address>;
}
// Immutable update helpers
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
function addItem<T>(array: readonly T[], item: T): readonly T[] {
return [...array, item];
}
function removeItem<T>(array: readonly T[], index: number): readonly T[] {
return [...array.slice(0, index), ...array.slice(index + 1)];
}
function updateItem<T>(
array: readonly T[],
index: number,
update: Partial<T>
): readonly T[] {
return array.map((item, i) => (i === index ? { ...item, ...update } : item));
}
// Immer-like pattern
type Producer<T> = (draft: T) => void;
function produce<T extends object>(base: T, producer: Producer<T>): T {
const draft = structuredClone(base);
producer(draft);
return draft;
}
// Utilisation
const state = { count: 0, items: [] };
const nextState = produce(state, draft => {
draft.count += 1;
draft.items.push('new item');
});
Freezing
function deepFreeze<T extends object>(obj: T): Readonly<T> {
Object.keys(obj).forEach(key => {
const value = (obj as any)[key];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
8. Option/Optional
Définition
Le type Option (ou Optional) représente une valeur qui peut être présente ou absente, évitant les null checks explicites.
type Optional<T> = T | null | undefined;
// Wrapper plus sophistiqué
class Optional<T> {
private constructor(private value: T | null | undefined) {}
static of<T>(value: T | null | undefined): Optional<T> {
return new Optional(value);
}
static empty<T>(): Optional<T> {
return new Optional<T>(null);
}
map<U>(fn: (value: T) => U): Optional<U> {
return this.value != null
? Optional.of(fn(this.value))
: Optional.empty();
}
flatMap<U>(fn: (value: T) => Optional<U>): Optional<U> {
return this.value != null
? fn(this.value)
: Optional.empty();
}
filter(predicate: (value: T) => boolean): Optional<T> {
return this.value != null && predicate(this.value)
? this
: Optional.empty();
}
getOrElse(defaultValue: T): T {
return this.value != null ? this.value : defaultValue;
}
orElse(alternative: Optional<T>): Optional<T> {
return this.value != null ? this : alternative;
}
ifPresent(fn: (value: T) => void): void {
if (this.value != null) fn(this.value);
}
toArray(): T[] {
return this.value != null ? [this.value] : [];
}
}
// Utilisation
function findUser(id: number): Optional<User> {
const user = database.getUser(id);
return Optional.of(user);
}
const city = findUser(1)
.flatMap(user => Optional.of(user.address))
.map(address => address.city)
.getOrElse('Unknown');
9. Either/Try
Either with Try
// Try = Either<Error, T>
type Try<T> = Either<Error, T>;
function Try<T>(fn: () => T): Try<T> {
try {
return right(fn());
} catch (e) {
return left(e instanceof Error ? e : new Error(String(e)));
}
}
// Patterns d'utilisation
function parseAndValidate(json: string): Try<User> {
return Try(() => JSON.parse(json))
.flatMap(data => Try(() => validateUser(data)));
}
// Pattern matching
function handleResult(result: Try<User>): string {
if (result.isLeft()) {
return `Error: ${result.value.message}`;
}
return `User: ${result.value.name}`;
}
// Combinators
function mapError<L, R, L2>(either: Either<L, R>, fn: (left: L) => L2): Either<L2, R> {
return either.isLeft()
? left(fn(either.value))
: right(either.value);
}
function bimap<L, R, L2, R2>(
either: Either<L, R>,
leftFn: (left: L) => L2,
rightFn: (right: R) => R2
): Either<L2, R2> {
return either.isLeft()
? left(leftFn(either.value))
: right(rightFn(either.value));
}
10. Lenses
Définition
Un Lens est un accesseur fonctionnel qui permet de focus et de modifier une partie d'une structure de données immuable.
interface Lens<S, T, A, B> {
get: (source: S) => A;
set: (value: B, source: S) => T;
}
// Lens simplifié (S = T, A = B)
type SimpleLens<S, A> = Lens<S, S, A, A>;
function lens<S, A>(
getter: (source: S) => A,
setter: (value: A, source: S) => S
): SimpleLens<S, A> {
return { get: getter, set: setter };
}
// Création de lenses
const nameLens = lens<User, string>(
user => user.name,
(name, user) => ({ ...user, name })
);
const streetLens = lens<Address, string>(
addr => addr.street,
(street, addr) => ({ ...addr, street })
);
// Composition
function composeLens<S, M, A>(
outer: SimpleLens<S, M>,
inner: SimpleLens<M, A>
): SimpleLens<S, A> {
return lens(
source => inner.get(outer.get(source)),
(value, source) => outer.set(inner.set(value, outer.get(source)), source)
);
}
const userStreetLens = composeLens(
lens<User, Address>(
user => user.address,
(address, user) => ({ ...user, address })
),
streetLens
);
// Utilisation
const user: User = { id: 1, name: 'Alice', address: { street: '123 Main St' } };
const newName = nameLens.set('Bob', user);
// { id: 1, name: 'Bob', address: { street: '123 Main St' } }
const newStreet = userStreetLens.set('456 Oak Ave', user);
// { id: 1, name: 'Alice', address: { street: '456 Oak Ave' } }
// Over (modifier via une fonction)
function over<S, A>(lns: SimpleLens<S, A>, fn: (value: A) => A, source: S): S {
return lns.set(fn(lns.get(source)), source);
}
const uppercaseName = over(nameLens, name => name.toUpperCase(), user);
11. Transducers
Définition
Les transducers sont des fonctions de transformation de données qui sont indépendantes de la source et de la destination, permettant une composition efficace.
type Reducer<T, R> = (acc: R, value: T) => R;
type Transducer<T, U, R> = (next: Reducer<U, R>) => Reducer<T, R>;
function map<T, U>(fn: (value: T) => U): Transducer<T, U, any> {
return <R>(next: Reducer<U, R>) =>
(acc: R, value: T) => next(acc, fn(value));
}
function filter<T>(predicate: (value: T) => boolean): Transducer<T, T, any> {
return <R>(next: Reducer<T, R>) =>
(acc: R, value: T) => predicate(value) ? next(acc, value) : acc;
}
function take<T>(n: number): Transducer<T, T, any> {
return <R>(next: Reducer<T, R>) => {
let count = 0;
return (acc: R, value: T) => {
if (count++ < n) return next(acc, value);
return acc;
};
};
}
// Composition de transducers
function composeTransducers<T, U, V, R>(
t1: Transducer<T, U, R>,
t2: Transducer<U, V, R>
): Transducer<T, V, R> {
return <R>(next: Reducer<V, R>) => t1(t2(next));
}
// Utilisation
const transducer = composeTransducers(
map((x: number) => x * 2),
filter((x: number) => x > 5)
);
const result = [1, 2, 3, 4, 5].reduce(
transducer((acc: number[], x: number) => [...acc, x]),
[]
);
// [6, 8, 10] (2,4,6,8,10 filtrés > 5)
12. Comparaison OOP vs FP
| Aspect | OOP | FP |
|---|---|---|
| Unité de base | Objet (état + comportement) | Fonction |
| État | Mutable (via méthodes) | Immutable |
| Composition | Héritage, composition d'objets | Composition de fonctions |
| Polymorphisme | Sous-typage, interfaces | Type classes, génériques |
| Effets de bord | Communs | Isolés (IO Monad) |
| Boucles | for, while | Récursion, map, reduce |
| Null safety | Optionnelle (null checks) | Option, Maybe |
| Erreurs | Exceptions | Either, Try |
| Parallélisme | Complexe (synchronisation) | Simple (pas d'état mutable) |
Quand choisir ?
Préférer FP quand :
- Transformations de données complexes
- Calcul parallèle
- Pipelines de traitement
- Systèmes où la prédictibilité est cruciale
Préférer OOP quand :
- UI et interfaces graphiques
- Systèmes avec état complexe
- Domaines avec identités fortes
- Équipes familières avec les patterns classiques
Approche hybride
La plupart des projets modernes utilisent un mélange des deux :
// OOP pour la structure, FP pour la logique
class OrderService {
constructor(private repository: OrderRepository) {}
async processOrder(order: Order): Promise<Try<ProcessedOrder>> {
return Try(() => this.validate(order))
.flatMap(validOrder => this.calculateTotal(validOrder))
.flatMap(orderWithTotal => this.applyDiscounts(orderWithTotal))
.flatMap(finalOrder => this.repository.save(finalOrder));
}
private validate(order: Order): Order {
if (!order.items.length) throw new Error('Empty order');
return order;
}
private calculateTotal(order: Order): Order {
return { ...order, total: order.items.reduce(sum, 0) };
}
private applyDiscounts(order: Order): Order {
return pipe(
applyLoyaltyDiscount,
applySeasonalDiscount,
applyCouponDiscount
)(order);
}
}
13. Conclusion
Les patterns fonctionnels offrent une alternative puissante aux patterns orientés objet. Ils excellent dans la manipulation de données, la composition, et la gestion des effets de bord. La clé est de comprendre quand et comment les appliquer, souvent en combinaison avec d'autres paradigmes.
Points clés à retenir
- Functor : Transformation dans un contexte (map)
- Monade : Composition séquentielle dans un contexte (flatMap)
- Currying : Transformation de fonctions n-aires en unaires
- Composition : f ∘ g = construire du complexe avec du simple
- Immutabilité : Toujours créer, jamais muter
- Option/Either : Gérer l'absence et les erreurs sans exceptions
- Lenses : Focus immuable sur des parties de structures
- Transducers : Composition efficace de transformations