Penser fonctionnel avec F#

2020-01-08 par Badre BSAILA

F# (F désigne Fun) est un langage fonctionnel-orienté objet membre de la famille des langages de la plateforme .NET. Il a été créé par Don Syme de l’entité Microsoft Research UK, Cambridge (toujours architecturé par lui-même). Sa genèse a commencé en 2002 et était une des ripostes des défenseurs des langages fonctionnels fortement typés face à la montée en puissance de l’orienté objet durant la dernière décennie du 20ème siècle. Le développement de l’essentiel du socle F# a été mené entre 2004-2007. F# est open-source, cross-platform (Windows, Linux, MacOS) et peut être utilisé pour faire du développement web, mobile, desktop et Azure. Plusieurs EDI le supportent: VS, VS Code, Jet Brains Rider, et Emacs.

Paradigme fonctionnel

Commençons par rappeler les principaux paradigmes de programmation:

  • Impératif: cette catégorie de langages résolvent un problème en définissant un certain nombre d’instructions à exécuter avec un ordre bien définit et qui retournent un résultat, on peut classifier ces langages comme:

    • Procéduraux: organisent les instructions dans des procédures qu’on peut réutiliser pour résoudre un problème donné (C, Pascal).
    • Orientés objet: modélisent le monde sous forme d’objets avec un état interne et des actions, chaque instruction est en fait un appel à une action d’un objet (Java, C++, C#).
  • Déclarative: les langages déclaratives taclent les problématiques en déclarant ce qui doit être fait au lieu de le faire littéralement, on peut les organiser en tant que:

    • Fonctionnels: le résultat attendu est déclaré en tant qu’output d’une série de fonctions (LISP, F#, Haskell, OCaml).
    • Logiques: le résultat désiré est exprimé sous forme de faits et de règles à respecter (Prolog).

Pourquoi utiliser F#

  • Concis: le code n’est fourré ni d’accolades (remplacées par 4 espaces comme Python) ni de point-virgules (fin de ligne suffit). Oubliez les déclarations des types de variables et celles des fonctions (paramètres, retours), avec son puissant système d’inférence le compilateur F# est capable de déduire facilement le typage depuis le contexte d’utilisation.
  • Convenable: sa syntaxe est très intuitive pour créer des types de données complexes, traiter des listes, comparer les objets les plus complexes, créer des machines d’état,…etc.
  • Correcte: le désign du langage prévient les exceptions de références nulles, les valeurs sont immutables par défaut ce qui rend non-nécessaire l’utilisation des mécanismes de synchronisation comme les locks, mutex et sémaphores. Avec le pattern-matching et les unions, c’est quasi-impossible d’oublier le traitement de cas spécifiques pour vos données.
  • Parallélisable: la programmation asynchrone est de même qu’en C# (en fait F# a introduit le sucre syntaxique async en 2007, alors que C# ne l’a fait qu’en 2012, voir). F# expose des apis faciles pour créer un modèle d’acteurs. Programmation réactive et parallélisme sont aussi au menu. La synchronisation des threads n’a pas de place dans ce monde à cause de l’immutabilité par défaut des données.
  • Complet: vous pouvez accéder à l’ensemble des apis de .NET depuis F# comme si vous développiez avec C# ou VB.NET.

Avant-goût du langage

Guidelines pour la programmation fonctionnelle

  • Orienté fonction plutôt qu’objet.
  • Fonctions pures : les fonctions doivent être définit au sens mathématique du terme, ainsi une fonction ne doit jamais changer de retour si on utilise toujours les mêmes arguments (ex: une fonction qui utilise le date courante n’est pas pure). En plus, son évaluation ne doit pas avoir d’effet de bord (ce qui facilite la parallélisation des exécutions).
  • Privilégier les expressions (qui produisent une nouvelle valeur) à la place des instructions (qui en modifient).
  • Immutabilité des valeurs.

Résolution d’un problème avec C# et F#

Les données partagées représente un vrai défi pour la programmation parallèle, la solution la plus directe est l’utilisation des mécanismes de synchronisation des threads (lock, mutex, semaphores…etc) qui bloquent tous les autres threads quand un autre essaie de modifier une information (comme ça on ne fausse pas les calculs de ces derniers). Dans ce qui suit nous allons implémenter un compteur qui peut être modifié par plusieurs threads en C# et F#.

Ébauche de solution pour C#

La solution repose sur l’utilisation du mot clé lock:

MailboxProcessor à la rescousse pour F#

MailboxProcessor permet d’implémenter un modèle d’acteurs à l’équivalent du pooling avec RabbitMQ et Kafka (version plus légère), les acteurs remplacent les threads concurrents qui publient sur la file du MailboxProcessor de façon asynchrone pour demander la MAJ de la shared data, et la synchronisation avec le lock est remplacé par la consommation des messages publiés un à un.

Dans cette solution, nous avons produit un nouveau compteur pour chaque mise à jour au lieu de modifier toujours le même, ainsi nous avons privilégié les expressions sur les instructions et avons respecté l’immutabilité des valeurs.

Influences de F# sur C#

Vous allez être surpris dans la mesure où c’est F# qui a initié le travail sur:

  • les types génériques (dans C# 2.0).
  • le mot clé var (C# 3.0).
  • Task/async (C# 5.0) (il faut reconnaître que l’ébauche de ceci a commencé dans un autre language fonctionnel Haskell).
  • Tuple + Pattern matching (C# 7.0)
  • Les références non-nullables par défaut (C# 8.0).
  • LINQ est une approche purement fonctionnelle pour traiter les listes de données.

Conclusion

J’espère que je vous ai donné un peu d’avant-goût de ce qu’est un langage fonctionnel fortement typé comme F#. Si vous êtes intéressés par en savoir plus voilà quelques ressources intéressantes:

Share the love

alt Amour FSharp

Et joyeuse année 2020 de :tada: avec les meilleurs vœux.


Badre BSAILA, Ingénieur d'étude et développement .NET sénior