1. Introduction
La programmation fonctionnelle (PF) est un paradigme de programmation où le bloc de construction principal n'est ni l'objet ni la procédure/méthode, mais la fonction au sens mathématique. En PF, l'attention se porte sur la description de "ce qu'il faut calculer", plutôt que "comment le calculer".
Vous avez déjà rencontré des idées isolées de la PF quand vous avez travaillé avec des expressions lambda et LINQ. Mais où est la différence ? En pratique : l'OO décrit des objets et leurs interactions, la programmation procédurale — une suite d'étapes, et la PF — la composition de fonctions, la transmission du comportement comme valeur, le refus de modifier l'état (immutability) et l'absence d'effets de bord.
Pourquoi adopter ce nouveau paradigme ?
- Du code plus propre, prédictible et testable.
- Soutien simplifié du multithreading ("pas d'état — pas de problèmes").
- Concision et expressivité (moins de code = moins de bugs).
- Abstractions haut-niveau, facilement réutilisables.
Analogie
Imaginez qu'un restaurant reçoit la commande : "préparer une omelette". Le cuisinier impératif exécute une liste d'instructions : prendre des œufs, casser, battre, cuire. Le cuisinier fonctionnel dit : res = omelette(oeufs) — il manipule des fonctions, en s'abstrayant de l'état interne de la cuisine (bon, presque).
En C# on peut utiliser les deux approches. Ça rend le langage très flexible et puissant — surtout pour des projets réels.
Concepts clés de la PF
1. Fonctions d'ordre supérieur
Les fonctions peuvent être passées comme paramètres, retournées par d'autres fonctions et stockées dans des variables. Vous l'avez déjà fait avec des expressions lambda et des delegates. En PF, ces "fonctions sur des fonctions" sont la base de tout.
2. Fonctions pures
Une fonction est dite "pure" si son résultat dépend uniquement de ses paramètres et qu'elle ne modifie rien en dehors d'elle (pas d'effets de bord). Deux appels identiques avec les mêmes arguments donnent le même résultat.
3. Immuabilité (Immutability)
Les données ne sont pas mutées "sur place" : le nouvel état est un nouvel objet. Ça simplifie fortement le raisonnement sur le programme et aide en multithreading.
4. Absence d'effets de bord
La fonction n'écrit rien dans un fichier, ne change pas de variables globales, n'affiche rien — elle renvoie juste un résultat. Dans la vraie vie les effets de bord sont inévitables, mais on cherche à les isoler aux frontières du système.
5. Composition de fonctions
On peut construire une fonction à partir d'autres, comme des briques. Par exemple : filtrer les nombres positifs, prendre leurs carrés et les additionner. Chaque opération est une fonction séparée, et elles se combinent facilement (Where → Select → Sum).
2. PF en C#: de la théorie à la pratique
C# est un langage multiparadigme : il supporte très bien l'OO, l'approche procédurale et un style fonctionnel puissant (avec des lambdas, des delegates, des méthodes d'extension et LINQ).
Examinons avec l'exemple de notre application pédagogique
Supposons qu'on développe un programme qui travaille avec des listes de nombres et de chaînes. Notre tâche est d'appliquer différentes opérations à ces données dans un style fonctionnel.
Exemple 1 : Utiliser des fonctions d'ordre supérieur
// Applique une action à tous les éléments de la liste
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
Utilisation :
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // Paramètre-fonction
Vous voyez ? Une fonction peut être "manipulée" comme une variable ou passée comme valeur — un peu comme une pomme en cuisine !
Exemple 2 : Fonction pure
Une fonction qui ne change pas l'état du programme et qui dépend uniquement de son entrée :
int MultiplyByTwo(int x)
{
return x * 2;
}
- Ne dépend de rien d'extérieur.
- Ne modifie rien à l'extérieur.
- Pour x = 5 renverra toujours 10.
Comparez avec une fonction qui utilise et modifie une variable globale :
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
Ceci n'est plus une fonction pure — le résultat dépend de l'état externe, et elle le modifie.
Exemple 3 : Immuabilité des données
Au lieu de modifier les données d'entrée, on crée de nouvelles collections :
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
La liste d'entrée n'est pas du tout modifiée. En multithreading c'est particulièrement pratique : moins de locks et de races.
Exemple 4 : Composition de fonctions
Obtenir la somme des carrés de tous les nombres pairs :
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // Garder seulement les pairs
.Select(n => n * n) // Élever au carré
.Sum(); // Additionner
}
Lisible et déclaratif : chaque opération est une fonction distincte.
3. Nuances utiles
PF, LINQ et C#
LINQ est presque "la PF en pratique" pour les collections : vous utilisez des fonctions d'ordre supérieur (Where, Select, etc.), vous obtenez de nouvelles séquences sans muter les originales, et chaque transformation est une expression séparée. Le résultat est un IEnumerable<T>, qui décrit ce qu'on veut obtenir, pas comment itérer.
Table des analogies
| Impératif (procédural/OO) | Fonctionnel (LINQ/style PF) |
|---|---|
|
|
| «Muter» la collection | Obtenir une nouvelle collection |
| État (total += x) | Fonctions pures (xs.Sum()) |
| Décrire comment : «fais ceci» | Décrire : «ce que l'on veut obtenir» |
PF vs OO : deux mondes — un seul C#
Ce ne sont pas des camps adverses. Dans des projets C# réels on combine les deux : le modèle du domaine (domain model) se construit souvent avec des classes (OO), tandis que le traitement des collections, l'agrégation et les transformations se font en style fonctionnel via LINQ, des lambdas et des méthodes d'extension.
Vos connaissances sur les delegates sont directement utiles : Func<T, TResult>, Predicate<T>, Action<T> — ce sont des briques typiques du style PF.
Fonction universelle de filtrage :
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
Appels :
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
Au lieu d'avoir plein de méthodes similaires avec des conditions différentes — une fonction universelle suffit.
Pourquoi les employeurs et les interviews aiment les développeurs PF ?
- La PF aide à tester des petits blocs de code sans démarrer tout le système.
- La logique est plus facile à maintenir : moins d'états = moins de sources d'erreurs.
- Écrire du code parallèle et asynchrone est plus simple — pas d'état global, moins de data races.
Et comment ne pas devenir fanatique ?
Oui, la PF est puissante. Mais C# n'est pas un langage purement fonctionnel, et toutes les tâches ne demandent pas une pureté absolue. N'ayez pas peur des variables locales et d'une mutation raisonnable quand c'est approprié. L'important est la lisibilité, la prévisibilité et la testabilité. Les éléments de la PF sont des outils, pas une religion.
4. Erreurs typiques du débutant
Il est très facile de tomber sur du code qui a l'apparence fonctionnelle mais qui en réalité ne l'est pas.
Par exemple, une fonction retourne une nouvelle collection, mais mutile la liste originale en cours de route — cela viole le principe d'immutabilité et casse les attentes du code appelant.
Autre exemple : une expression lambda accède à une variable externe et la modifie. Dans le paradigme fonctionnel c'est considéré comme un effet de bord et rend le comportement du code moins prévisible.
Le compilateur C# ne vous arrêtera pas : le langage permet les deux. C'est pourquoi dans les pratiques PF il est important de s'assurer que la fonction "vit pour elle-même", ne modifie rien à l'extérieur et ne lit rien en dehors de ses arguments.
GO TO FULL VERSION