Publié le 16/05/2017 Par Gaël Dupire

Depuis une petite décennie, la programmation fonctionnelle suscite un regain d’intérêt de la part des communautés de développeurs. De nouveaux langages fonctionnels comme Elixir (2012), Clojure (2007) ou encore ELM (2012) sont apparus. Le concept de programmation fonctionnelle n’est pourtant pas nouveau. Le premier langage du genre, le LISP, est né quatre ans seulement après le FORTRAN. Ce qui en fait un des plus vieux langages de programmation. Le plus frappant c’est qu’il est toujours utilisé. Plusieurs caractéristiques peuvent expliquer sa longévité et sa pertinence.

Des fonctions pures

D’abord en programmation fonctionnelle, les données sont immuables par défaut. Ce qui présente quelques avantages. En multithreading par exemple, l’utilisation de mécanismes de synchronisation (souvent bloquants) n’est pas nécessaire. Cela simplifie la gestion du code en éliminant de fait les “deadlock et les “race condition”. Par ailleurs, les fonctions ne dépendant que des arguments d’entrées et de leur(s) variable(s) locale(s), leur exécution est donc parfaitement prévisible… On parle alors de fonctions pures, car sans effet de bord. Le code est d’autant plus compréhensible.

Prenons un exemple concret : si on veut convertir une quantité d’argent d’une monnaie à une autre, on peut utiliser le petit code suivant (en F#) :

type ForexChange = { Rate : double }

type Converter = { MeasuredCurrency : string ;
                   MeasuringCurrency : string ;
                   ChangeRate : ForexChange }

let convertFromMeasured valueInMeasuredCurrency converter = 
    valueInMeasuredCurrency / converter.ChangeRate.Rate

let convertToMeasured valueInMeasuringCurrency  converter = 
    valueInMeasuringCurrency * converter.ChangeRate.Rate

Que se passe-t-il ? Le type Converter définit les données nécessaires au change. Les fonctions ne dépendent que de la valeur que l’on souhaite convertir et de la version des données utilisées. Même en liant les données à une fonction, toute modification passera par la création d’une nouvelle entité évitant ainsi les effets de bord.

Dans les deux cas, il faudra créer une nouvelle entité de type Converter pour prendre en compte un changement de taux. N’importe quel code client utilisant l’ancienne version pourra reproduire une même exécution et obtenir le même résultat.

Mais attention, devoir recréer cette nouvelle entité est une contrainte qui, sur des structures plus complexes (un arbre par exemple), peut conduire à une consommation plus élevée de ressources système (plus de garbage collection, utilisation accrue du CPU et de la mémoire managée). Notez que beaucoup de langages fonctionnels utilisent des garbage collectors. Il faut également faire attention à ne pas utiliser des informations périmées dans le cas de données très volatiles.

Un langage concis

Les langages fonctionnels sont très concis. Dans notre exemple, on peut voir que le code F# nécessite peu de lignes de code et peu de mots clefs. Jugez plutôt. Voici ce que l’on devrait écrire en C# pour notre exemple de conversion entre monnaies :

Exemple de conversion en C#

public class ForexConverter
{
	public string MeasuredCurrency { get; }
	public string MeasuringCurrency { get; }
	private double _changeRate;

	public ForexConverter(string currency1, string currency2, double changeRate)
	{
	    MeasuredCurrency = currency1;
	    MeasuringCurrency = currency2;
	    _changeRate = changeRate;
	}

	public double ConvertFromMeasured(double valueInMeasuredCurrency)
	{
	    return valueInMeasuredCurrency / _changeRate;
	}

	public double ConvertToMeasured(double valueInMeasuringCurrency)
	{
	    return valueInMeasuringCurrency * _changeRate;
	}

	public void ChangeRate(double newChangeRate)
	{
	    Interlocked.Exchange(ref _changeRate, newChangeRate);
	}
}

D’autres éléments de syntaxe peuvent compléter l’arsenal visant à plus de clarté. Comme la possibilité de pipeliner des fonctions (envoyer le résultat d’une fonction dans l’entrée de la suivante). Si je souhaite avoir la liste des 50 premiers nombres pairs multiples de 3 je l’écrirai ainsi :

let someTreatment () =
    let l = [0..99]
    l |> List.filter (fun i -> i%2=0) |> List.map (fun i -> i*3)

Vous le voyez, en programmation fonctionnelle, on peut rapidement créer des codes concis et simples à appréhender. Le typage est un atout qui permet non seulement de structurer puissamment les sources mais aussi d’apporter une souplesse appréciable. On le voit, ces langages sont particulièrement adaptés à l’écriture de code business ou d’outils utilisant des flux de données.

Efficace, cependant…

Si on prend le cas classique où la mutabilité se montre très utile, les tris sur place, il est évident que ce n’est pas naturel de le faire avec des langages fonctionnels. Il est cependant possible de forcer la mutabilité en F#. On peut utiliser le mot clé mutable dans le binding let afin de définir une variable. On peut également utiliser des tableaux qui sont des structures de données mutables dans ce langage. Vous trouverez des exemples d’implémentation de tri en F# ici.

Cependant forcer la mutabilité complexifie le code et va à l’encontre des principes de la programmation fonctionnelle. Dans ce genre de cas il faut savoir utiliser les bons outils en se tournant vers le bon paradigme. Comme dans tout projet, il me paraît nécessaire d’apprécier un système dans sa globalité avant de décider d’utiliser uniquement ce type de langage. Je crois même qu’il ne faut pas s’interdire d’utiliser à la fois la POO et la programmation fonctionnelle.

Le mariage C#/F# semble d’ailleurs prometteur. Les deux peuvent cohabiter nativement au sein de l’écosystème .NET. C’est d’ailleurs un mélange que l’on trouve régulièrement dans des bibliothèques à destination du F#. L’architecture des projets peut aussi aider à marier des projets utilisant les différents paradigmes. Un projet en micro service permettra facilement l’écriture de code dans l’un ou l’autre des paradigmes, en fonction des besoins.

En somme, la programmation fonctionnelle peut répondre à de nombreuses problématiques actuelles. Elle est pourtant perçue comme une solution limitée dans certains champs. Mais d’expérience je sais que son usage a changé ma façon d’appréhender la production de code objet. Je tâcherai de faire partager mon expérience dans de prochains posts.

——–

Si le sujet vous intéresse, ci-dessous, les autres articles du dossier sur la programmation fonctionnelle, co-écrit avec Clément Carreau :
Créer des types fonctionnel en F# et en Scala
– Le Pattern Matching, le Demolition Man intelligent

Pas encore de commentaires

Publier un commentaire

Auteur

Gaël Dupire

Gaël a débuté sa carrière en tant que Software Consultant avant de devenir Senior Software Development Engineer à la Société Générale (SGCIB) puis chez Meritis il y a 10 ans.

Gaël est diplômé de l’UTC (Doctorat de Mathématiques appliquées).

Il est passionné par les maths et leur utilisation, Il adore coder, aussi bien en .Net, python, C++ ou Matlab.