Blog Acensi
Ecran de code

Pourquoi vous devriez apprendre un langage fonctionnel

1. Introduction

Etant un développeur .NET, les exemples de code fournis dans cet article sont en langages C# et F#. Ceci étant dit, ils sont suffisamment simples pour être compréhensibles par (à peu près) n’importe qui. De plus, les principes exposés par la suite restent valables quel que soit votre langage de prédilection.
Je vais dans un premier temps rappeler brièvement les grands principes régissant les programmations impérative et fonctionnelle. Ensuite, j’essaierai de montrer, au travers de l’historique du langage C#, à quel point la programmation fonctionnelle est devenue incontournable. Enfin, j’expliquerai pourquoi des langages purement fonctionnels ont toutes les chances de s’imposer dans les prochaines années.

2. Programmation impérative

La programmation impérative est un paradigme décrivant l’exécution d’un programme comme un ensemble d’instructions permettant de faire évoluer l’état du système représenté par ledit programme. Elle met en œuvre trois grands principes, définis ci-dessous.

2.1. Affectation

Une opération d’affectation consiste à attribuer une valeur à une variable, une variable représentant une information en mémoire. Cette dernière pourra être lue et modifiée autant de fois que nécessaire.

int i = 100;

2.2. Boucle

Une boucle permet la répétition d’un ensemble d’instructions, jusqu’à ce qu’une condition soit vraie, ou bien un certain nombre de fois.

for(int i = 0; i < 5; i++)
    Console.WriteLine(i);

2.3. Branchement conditionnel

Un branchement conditionnel est une instruction permettant, si une condition donnée est vraie,  l’exécution d’un ensemble d’instruction.

int value = GetValue();
if (value > 10)
    Console.WriteLine(value);

3. Programmation fonctionnelle

3.1. First-class function

Le terme français est « fonction de première classe », mais il est assez peu employé dans la littérature… Cette caractéristique des langages fonctionnels permet à une fonction de prendre en paramètre une autre fonction, et/ou retourner une fonction en tant que résultat. Une fonction peut également être assignée à une variable.
Dans l’exemple ci-dessous, en langage F#, la fonction compute prend en paramètre un nombre et une autre fonction.

let compute n f =
    let n = n * n
    f n
let result = compute 6 (fun n -> n + 1)

Il est possible d’aller plus loin avec la notion de fonction appliquée partiellement, qui consiste à fixer un ou plusieurs paramètres d’une fonction donnée. Par exemple, pour définir une fonction compute spéciale pour laquelle le paramètre n a toujours la valeur 6 :

let compute6 f = compute 6 f
let result = compute6 (fun n -> n + 1)

3.2. Fonction pure

Une fonction pure n’a le droit d’utiliser que les paramètres qui lui sont fournis en entrée, les variables de travail dont elle a besoin, ainsi que… d’autres fonctions pures. Une fonction pure ne peut donc pas modifier l’état d’un autre objet (se référer au concept d’immutabilité défini un peu plus bas). Elle ne peut pas non plus appeler des méthodes ayant un effet de bord : par exemple, en C#, System.Console.WriteLine() écrit du texte dans la console, modifiant du même coup l’état de la console (de manière générale, toute fonction réalisant des entrées/sorties est dite à effet de bord, et ne peut donc pas être utilisée par une fonction pure).

let addOne n = n + 1

3.3. Récursivité

Une fonction récursive est une fonction qui, pour retourner le résultat de son calcul, nécessite de s’invoquer elle-même avec d’autres paramètres. Cette invocation pourra elle-même entraîner une autre invocation, et ce autant de fois que nécessaire.
La récursivité permet souvent l’écriture plus concise d’algorithmes, et fonctionne en analysant les valeurs fournies en paramètres aux fonctions utilisées.
Voici un exemple de fonction récursive, en F#, permettant le calcul d’une valeur de la suite de Fibonacci.

let rec fib n =
    match n with
    | 0 -> 1
    | 1 -> 1
    | x -> fib(n - 1) + fib(n - 2)

3.4. Immutabilité

Le concept d’immutabilité se rapporte à l’état des objets crées par un programme. Un objet immutable ne peut pas être modifié après sa création. En d’autres termes, si vous avez besoin de faire évoluer l’état d’un objet immutable O1… vous ne pouvez tout simplement pas le faire ! Vous serez dans l’obligation de construire un nouvel objet O2 comprenant la modification que vous souhaitez appliquer à O1.
Voici un exemple de classe immutable en C# :

public class Person
{
    private readonly string _firstName;
    private readonly string _lastName;
    public Person(string firstName, string lastName)
    {
        _firstName = firstName;
        _lastName = lastName;
    }
    public string FirstName
    {
        get { return _firstName; }
    }
    public string LastName
    {
        get { return _lastName; }
    }
}

Et maintenant l’équivalent en F# :

type Person = { FirstName: string; LastName: string }

3.5. Pattern matching

Le terme français est « filtrage par motif », mais étant donné qu’il est peu usité… Evitez-le ! Le pattern matching est un concept de la programmation fonctionnelle permettant de diminuer le besoin de recourir à des variables spécifiques, et même à la définition de classes ou de types spécifiques. Comment ? En reconnaissant les constituants d’un motif particulier.
L’exemple déjà donné un peu plus haut, calculant un élément de la suite de Fibonacci, utilise le pattern matching dans son implémentation.

let rec fib n =
    match n with
    | 0 -> 1
    | 1 -> 1
    | x -> fib(n - 1) + fib(n - 2)

4. C# : vers un langage fonctionnel

Je vous propose maintenant de nous intéresser à l’historique du langage C#. A sa sortie, en 2000, il s’agissait d’un langage impératif permettant l’application des principes de la POO (programmation orientée objet). Depuis, il a beaucoup évolué, en intégrant notamment nombre de concepts liés à la programmation fonctionnelle.
Fin 2007 (cela ne rajeunit pas !), la version 3.0 du langage C# est sortie. Elle intégrait les principes listés ci-dessous.

4.1. Inférence de types

Certains d’entre vous fronceront les yeux en voyant ce point, et ils auront raison : l’inférence de types n’est pas forcément liée à la programmation fonctionnelle, même si elle en est souvent une caractéristique. Mais cela ne définit toujours pas ce qu’est l’inférence de types ! Il s’agit d’un concept, applicable uniquement aux langages typés statiquement, permettant au compilateur de déduire le type d’une valeur ou d’une expression. Par exemple, à la place de

Dictionary<string, List<int>> dictionary = new Dictionary<string, List<int>>();

il est possible d’écrire

var dictionary = new Dictionary<string, List<int>>();

L’inférence de types est indispensable pour pouvoir utiliser des types anonymes, présentés ci-après.

4.2. Types anonymes

Un type anonyme permet la création d’objets sans qu’il soit nécessaire de définir une classe ou une structure pour cela. Un exemple valant mieux qu’un long discours, voici ce que cela peut donner :

var person = new { FirstName = "Jean", LastName = "DUPONT" };

Les objets anonymes sont immutables : ils sont définis une fois pour toutes, et ne peuvent donc plus être modifiés par la suite.

4.3. Expressions lambda

Vous rencontrerez plus souvent dans la littérature le terme « closure ». Les expressions lambda sont l’une des pierres angulaires de la programmation fonctionnelle. Il s’agit d’un concept très puissant permettant de définir des fonctions de manière anonyme. Par exemple, une lambda expression permettant de calculer le carré d’un nombre pourrait être définie ainsi :

x => x * x

4.4. LINQ To Objects

L’inférence de types, les types anonymes et les expressions lambda ont été des bases pour bâtir LINQ, qui permet de requêter des données d’une manière analogue à celle qui pourrait être employée dans un langage fonctionnel : en utilisant des fonctions qui elles-mêmes prennent en paramètres d’autres fonctions (expressions lambda) et peuvent travailler sur des objets anonymes, donc immutables.
Exemple : nous disposons d’une liste contenant les nombres de 1 à 10, et nous voulons calculer la somme des éléments pairs la constituant.

var numbers = new List<int>();
for (int i = 0; i < 10; i++)
    numbers.Add(i);

Une solution impérative serait :

int total = 0;
foreach(int number in numbers)
{
    if (number % 2 == 0)
        total += number;
}

Voici maintenant une solution plus fonctionnelle, basée sur LINQ To Objects :

int total = (from number in numbers
             where number % 2 == 0
             select number).Sum();

4.5. Programmation asynchrone

Quelques années après, des fonctionnalités directement intégrées au langage C# ont permis d’effectuer de la programmation asynchrone. Cela est possible grâce aux mots-clés async et await. Par exemple, pour récupérer le contenu HTML de la page d’accueil de Google, il suffit d’écrire :

async Task<string> FetchData()
{
    var client = new WebClient();
    return await client.DownloadStringTaskAsync(new Uri("http://www.google.fr"));
}

Ces fonctionnalités asynchrones sont issues de travaux effectués dans le langage F# : les workflows asynchrones, tirant profit des caractéristiques fonctionnelles du langage, notamment l’immutabilité…

4.6. Syntaxe plus concise

La version 6.0 du langage C# permettra d’écrire très simplement des classes immutables. Voici un exemple :

public class Person(string firstName, string lastName)
{
    public string FirstName { get; } = firstName;
    public string LastName { get; } = lastName;
}

Cette nouvelle syntaxe est quand même beaucoup plus concise que l’actuelle ! Comparez avec l’exemple qui a déjà été donné un peu plus haut :

public class Person
{
    private readonly string _firstName;
    private readonly string _lastName;
    public Person(string firstName, string lastName)
    {
        _firstName = firstName;
        _lastName = lastName;
    }
    public string FirstName
    {
        get { return _firstName; }
    }
    public string LastName
    {
        get { return _lastName; }
    }
}

5. Processeurs multi-cœurs

De nos jours, la plupart des smartphones vendus dans le commerce disposent d’au moins deux cœurs. Ne parlons même pas des PC de dernière génération ! Quelle que soit la plate-forme ciblée : serveur x86 Windows ou Linux, Windows Phone, iOS, Android, etc., les applications tournant dessus doivent exploiter au maximum les cœurs disponibles. Naturellement, cela implique de la programmation asynchrone et/ou parallèle. Et, souvent, des problèmes de concurrence d’accès aux données : pour résumer, deux threads accèdent exactement au même moment à la même information en mémoire, et l’un d’entre eux (voire les deux !) doit la modifier. Ce genre de bug peut réellement s’avérer cauchemardesque à diagnostiquer et à corriger, car à chaque exécution d’un programme faisant intervenir plusieurs threads, ces derniers auront des interactions différentes : le cas évoqué ci-dessus de deux threads impliqués dans un problème de concurrence d’accès aux données ne se produira pas toujours, en fait il a toutes les chances de se produire très rarement… Ce n’est pas pour rien que certaines sociétés se sont spécialisées dans la résolution de ce genre de problème.
La programmation parallèle est un art difficile du fait de la mutabilité des données en mémoire. Voilà la principale raison pour laquelle les langages fonctionnels suscitent tant d’intérêt : ils sont basés sur le concept d’immutabilité. A première vue cela peut paraître fou : comment développer des applications en n’utilisant que des données immutables et de fonctions pures ? C’est pourtant possible, la preuve : depuis de nombreuses années, un langage fonctionnel, Erlang, est très utilisé dans des contextes avec une forte concurrence et des contraintes temps réel…
Si, comme moi, vous avez essentiellement travaillé avec des langages impératifs, vous allez devoir revoir plusieurs de vos habitudes :

  • Remplacer les boucles par de la récursivité
  • Renoncer aux itérateurs, ce qui a pour conséquence… de remplacer certaines boucles par de la récursivité
  • Utiliser sur vos listes et tableaux des fonctions telles que map ou fold

J’ai choisi d’apprendre F#, mais Scala, Erlang, Haskell… sont tout aussi pertinents, à vous de choisir !

Conclusion

Cet article a montré comment le langage C# a évolué pour intégrer de nombreux concepts issus du monde fonctionnelle, mais il aurait très bien pu utiliser Java comme exemple. La programmation fonctionnelle est une tendance de fond, permettant de mieux exploiter les capacités des processeurs multi-cœurs. A l’heure du cloud, où le matériel doit être utilisé au mieux et les traitements massivement parallèles sont de mise, apprendre un langage fonctionnel – ainsi les paradigmes associés – est une bonne idée, voire une nécessité, pour tout développeur !

Pourquoi ce blog ?

Pour permettre à nos consultants et experts techniques de partager leurs connaissances et retours d’expérience autour des sujets qui les passionnent. Ce blog, intégralement écrit par eux, a pour vocation d’être un véritable lieu d’échanges et d’apprentissage.

Alors n’hésitez pas à commenter nos articles pour rejoindre la conversation !

Une suggestion ?

Si vous avez des idées pour améliorer ce blog, nous sommes à l’écoute de vos remarques. Vous pouvez nous écrire via le formulaire de contact qui se trouve en bas de page.

Bonne visite !