Développer des applications Big Data requiert la maîtrise du paradigme de programmation fonctionnelle. La programmation informatique classique n’est pas suffisante, car lorsqu’on veut traiter les données à large échelle dans un cluster, on fait face à des contraintes qui ne peuvent pas être résolues à l’aide des simples outils de la programmation informatique classique.
Comme vous le verrez, il existe plusieurs paradigmes de programmation. Mais que vous soyez déjà un développeur ou un simple débutant, il est indispensable (si ce n’est vital) pour vous d’apprendre la programmation fonctionnelle si vous souhaitez vous travailler dans le Big Data (spécialement si vous souhaitez vous orienter vers les métiers de développement du Big Data comme Data Engineer ou développeur Big Data).
Quoiqu’il existe depuis les années 1930 avec le lambda-calcul, ce n’est que récemment avec l’apparition des problématiques du Big Data (et surtout la sortie du langage Scala) que le paradigme de programmation fonctionnelle a gagné en popularité. Le premier langage fonctionnel, Lips, a vu le jour en 1958 !
Dans ce tutoriel, nous allons vous aider à apprendre la programmation fonctionnelle. A la fin du tutoriel, vous aurez compris les principes généraux de ce paradigme et le procédé à suivre pour développer des applications selon lui. Ne vous inquiétez pas, ça a l’air compliqué dit comme ça, mais c’est plus simple que cela ne le parait.
1 – Les principes fondamentaux de la programmation fonctionnelle
Actuellement, le paradigme le plus utilisé dans le développement applicatif c’est la programmation orientée objet. Ce paradigme fonctionne très bien, mais rencontre certains problèmes qui sont préjudiciables à certaines problématiques (notamment celles du Big Data).
De nos jours, les applications sont de plus en plus volumineux (en scripts de code), et de plus en plus complexes. Pour coder ces applications, les développeurs s’appuient sur des patrons de conception (design patterns) comme le MVC (Modèle-Vue-Controleur) ou le MVMM.
Malheureusement, ces patrons de conceptions sont plus élégants en théorie qu’en pratique. Ils sont très difficiles à suivre et rendent le développement applicatif très complexe. Pour s’épargner cette complexité, la plupart des développeurs préfèrent abandonner ces patrons de conception. Cela leur libère plus de temps pour le développement, mais a pour inconvénient de rendre les applications instables d’un point de vue architectural.
C’est là que la programmation fonctionnelle intervient. Elle se présente comme une solution de simplification au développeur. Comme son nom l’indique, elle repose sur la notion de fonction. C’est-à-dire qu’elle fait en sorte de fractionner le programme en des portions plus simples et plus petites pour constituer par la suite l’ensemble du programme final. Ces portions sont des fonctions.
Pour illustrer, c’est comme lorsque vous achetez un meuble chez Ikéa. Le meuble est livré dans un carton, vous avez à votre disposition toutes les pièces qui le constituent et vous n’avez qu’à le monter en suivant la notice inclue dans le carton. C’est aussi simple que ça !
Le cœur de la programmation fonctionnelle, ce sont les fonctions. On peut On peut les assigner à une variable, on peut les passer en paramètre dans une autre fonction, on peut même les utiliser comme valeur de retour d’une fonction.
C’est cette grande simplification qui fait en partie toute la force de ce paradigme de programmation; on n’a pas besoin de se fatiguer avec plusieurs concepts complexes et les interactions entre elles. Ici, l’application n’est composée que d’une seule unité de découpage : la fonction.
Voyons ensemble les « buiding blocks » (blocs et concepts) qui régissent le développement applicatif en programmation fonctionnelle.
Concept #1 : les fonctions pures
Pour comprendre le concept de fonction pure, regardez cette exemple :
int a=1 ;
int f(a,b) {
a = a + b ;
return a ;
}
Dans ce cas si b=1, f (1, 1) = 2. Et si l’on exécutait une deuxième fois f (1, 1), le résultat devient alors 3. C’est un exemple typique d’une fonction impure. Regardons maintenant cet exemple :
int f(a,b) {
a = a + b ;
return a ;
}
Pour cette fonction, f (1, 1) = 2. À la deuxième exécution, fonction (1, 1) reste 2. C’est cela que l’on appelle fonction pure.
Donc, une fonction est pure si et seulement si elle ne dépend que de ses paramètres. C’est-à-dire, la valeur de retour reste la même. Et cela même si la fonction est exécutée à plusieurs reprises au cours du programme. Ce concept se rapproche un peu du concept d’idempotence utilisé en Big Data Streaming.
En d’autres termes, aucune interaction extérieure n’est autorisée. Cela veut dire qu’aucune variable globale n’est utilisée. Les fonctions qui changent d’état au cours du temps sont interdites. D’où son nom de fonction pure.
Par exemple, Math.pow() est une fonction pure tandis que Radom() et Date.Now sont impures.
Concept #2 : l’immutabilité
Le principe d’immutabilité est une autre caractéristique propre au paradigme de programmation fonctionnelle. En programmation orientée objet, au cours de l’application, on a tendance à changer e permanence l’état (ou la valeur) d’une variable. Par conséquent, les fonctions aussi changent d’état. C’est ça que l’on appelle la mutabilité.
En programmation fonctionnelle, les fonctions ne changent pas d’état, ce qui fait qu’on peut les utiliser comme des constantes. Les fonctions restent intactes. Les variables sont fixes. Si l’on fait appel à la fonction, on ne fera que les copier dans la fonction qui va l’utiliser.
Dis comme ça, vous ne voyez nécessairement pas l’intérêt de l’immutabilité, mais en Big Data ce principe a une importance capitale !
En effet, dans le monde des bases de données et l’exécution concurrente, les systèmes qui s’appuient sur le principe de la mutabilité ont le défaut de ne pas être tolérant aux pannes. Cela signifie que lorsqu’un système est basé sur le principe de mutabilité, les changements d’état (ou de données) à un moment donné sont irréversibles ! En cas d’erreur, on ne peut plus revenir en arrière. La mutabilité pose de sérieux problèmes dans toutes les problématiques où les données sont utilisées de façon concurrente, notamment les problématiques de base de données.
L’immutabilité résout ce problème et permet de conserver les valeurs de chaque état à tout instant de la progression du programme. Cela garantit qu’à chaque instant, on peut retracer la valeur précédente d’un état avant la modification par un autre processus. Celà est extrêmement utile dans les calculs distribués, les bases de données, l’exécution concurrente de processus.
Grâce à l’immutabilité, les programmes peuvent être distribués entre les cœurs d’un serveur multi cœur et les serveurs d’un centre de données. L’immutabilité permet de tirer parti des architectures de processeurs multi cœurs d’une façon générale. C’est pourquoi Scala est le langage de choix pour toutes les problématiques du Big Data et du Cloud, dans lesquelles il faut traiter de façon massivement distribuée les données.
Concept #3 : la composition
Vous savez maintenant qu’avec la programmation fonctionnelle, on n’utilise que des fonctions, en l’occurrence, des fonctions pures. Mais, à quoi ça sert de créer plusieurs fonctions si l’on ne s’en sert pas ?
Revenons à l’exemple du meuble, vous disposez des pièces qui vous sont livrées. Mais pour avoir le meuble en question, il faut le construire.
La composition est, en quelque sorte, pour la programmation fonctionnelle cette construction. C’est-à-dire l’utilisation de ces fonctions pour élaborer tout le programme.
Il sera donc possible d’assembler ces fonctions pour en faire de nouvelles. Les lier les unes avec les autres pour constituer une nouvelle fonction qui aura une valeur de retour plus précise. Ce dernier pourra lui aussi servir à la construction d’une autre encore plus grande. Et ainsi de suite.
Cela rejoint le principe d’immutabilité qui consiste à employer les fonctions de façon à garder le même état tout au long de l’exécution du programme.
Concept #4 : les fonctions anonymes
Imaginez que vous élaborez votre programme et que dans son architecture, plusieurs fonctions sont nécessaires. Cependant, dans toutes ces fonctions, il y en a qui ne sont utilisées qu’une seule fois. Quel serait l’intérêt de les déclarer en tant que tel ? C’est pour répondre à cela que les fonctions anonymes existent.
Comme son nom l’indique si bien, c’est une fonction qui est par définition anonyme. C’est-à-dire qu’il n’est pas désigné par un nom. Nous allons voir dans la pratique comment le créer.
Son utilisation présente plusieurs avantages comme une meilleure lisibilité du code et les déclarations écourtées. Elles sont intéressantes surtout lors des traitements des listes. Si la fonction n’est utilisée qu’une seule fois, il est mieux de le faire en anonyme. Voici un exemple de traitement de liste en scala en utilisant une fonction anonyme.
val resultat2 = names.count( s => s.startsWith("b"))
val liste2 = names.filter(s => s.startsWith("b"))
val ints = List(1,2,3)
val doubledInts = ints.map(_ * 2)
val doubledInts = ints.map(_ * 2)
val doubledInts = ints.map((i: Int) => i * 2)
val doubledInts = ints.map(i => i * 2)
val x = ints.filter(_ > 5)
val x = ints.filter((i: Int) => i % 2 == 0)
Concept #5 : la séparation des données et de leur traitement
Jusque-là, vous avez appris comment la programmation fonctionnelle traite les fonctions. Mais qu’en est-il de la manipulation des données ?
On a vu que pour la programmation fonctionnelle, la pureté des fonctions est essentielle. Ce qui veut dire que les modifications de données et les affectations d’état peuvent rapidement se compliquer !
En programmation fonctionnelle, les manipulations de données, c’est-à-dire, l’insertion et l’extraction de celles-ci, sont séparées de leurs traitements. La manipulation de données implique un changement d’état, ce qui est hors du principe de ce paradigme. Rappelons qu’une fonction n’est pure que si ce qu’elle contient est pur.
L’astuce est donc d’avoir le maximum de fonctions pures dans son programme, de laisser ces dernières traiter les données. Et de les passer dans une fonction impure juste pour les affectations (insertions et extractions).
Concept #6 : l’écriture lazy
La programmation fonctionnelle offre un style d’écriture de code particulier. Pour que vous compreniez celà, prenons un exemple de code en Scala.
val ssession = SparkSession.builder.config(rdd_kafka.sparkContext.getConf).enableHiveSupport().getOrCreate()
Le code que vous voyez ci-dessous est en fait la déclaration d’un objet en Scala, qui exécute en une opération 2 fonctions : la fonction getOrCreate() et la fonction enableHiveSupport().
Ce style d’écriture est appellé le Lazy computation, pour dire que les calculs sont “paresseux” par défaut avec le paradigme de programmation fonctionnelle. Dans les autres langages, il est impossible d’appeler successivement dans la même ligne de code, plusieurs fonctions. Pour chaque unité de code à appeler, il faut créer des variables supplémentaires dans lesquelles les stocker, pour les réutiliser ultérieurement.
En plus de cela, lors de la compilation de l’application, toutes ces variables seront exécutées (même si après elles ne sont pas utilisées).
Or, le « Lazy » en programmation fonctionnelle permet de n’exécuter les expressions que lorsqu’elles sont nécessaires. Cela améliore grandement les performances de l’application. Du coup, dans l’exemple précédent, le code n’est compilé que s’il est réellement appelé à un moment ou un autre du programme. On n’est pas comme dans d’autres langages comme Java où chaque fois, tout le code est exécuté, même lorsqu’on avait voulu exécuté seulement une partie précise du programme.
Voilà les 6 concepts clés du paradigme fonctionnel. Nous allons maintenant vous donner ses avantages et inconvénients.
Les avantages et inconvénients du paradigme fonctionnel
Comme tous les paradigmes de programmation, elle a ses avantages ainsi que ces inconvénients. Regardons cela de plus près.
Les avantages
Parmi les avantages que la programmation fonctionnelle peut avoir, voici quelques unes :
- Elle évite les effets de bord grâce aux fonctions pures et à l’immutabilité
- Les applications sont plus courtes, et donc plus facile à maintenir
- Les bugs sont moins fréquents, en plus si les codes sont bien testés
- Les codes sont faciles à tester, et cela, quelque soit leur complexité
- La maintenance des codes est d’autant plus facile et prend moins de temps
- Le programme en entier est plus lisible
- On peut l’utiliser en combinaison avec la programmation orientée objet et d’autres paradigmes de programmation.
Les inconvénients
Malgré sa performance et son efficacité, elle présente aussi ses points faibles :
- On ne peut pas modifier les variables, ce qui complique un peu le traitement des données et rend l’écriture des programmes un peu plus complexe
- Écrire des programmes fonctionnels requiert une certaine pratique compte tenu des principes du paradigmes, qui changent des paradigmes habituels
- Les concepts du paradigme ne sont pas suffisants pour couvrir certains cas d’usage (le développement applicatif général, qui est plus simple avec le paradigme orienté objet). A notre sens, ce paradigme est approprié uniquement lorsqu’on travaille sur des problématiques liées à l’accès concurrent à des états (comme le Big Data).
Programmation fonctionnelle ou orienté objet ?
Malgré le fait que la programmation orientée objet est plus vulgarisée que le paradigme fonctionnel, les deux combinés permettent de faire des choses extraordinaires. Nous avons établi ici ce petit comparatif pour vous permettre de voir les différences fondamentales entre les 2. Nous tenons à préciser (et cela est important) que ce tableau comparatif n’est pas destiné à vous faire choisir l’un ou l’autre des paradigmes, mais à vous aider à comprendre les individualités de chacun afin que vous puissiez mieux vous en servir, car comme nous l’avons dit plus haut, dans de vrais projets Big Data, ces deux paradigmes sont souvent utilisés en parallèle.
Programmation fonctionnelle | Programmation orientée objet |
---|---|
variables immutables | variables mutables |
Possibilité de programmer en parallèle | Programmation parallèle non-prise en compte dans le paradigme |
Se base sur la notion de fonction | Se base sur la notion d’objet |
Utiliser la récursivité pour les itérations | Utilise les boucles pour les itérations |
Pas de changement d’état | Changement d’état existant |
Pas d’effet de bord | Effet de bord possible |
Idéale pour les traitements de données volumineuses | Idéal pour le développement des applications mono-thread |
Lisibilité du code sur les longs programmes | Code moins lisible lorsque l’application deviens vaste |
Compréhension difficile pour les non initiés | Compréhension abordable pour tous |
Moins performant dans certains cas | S’adapte plus facilement à l’environnement |
Plus adaptée au milieu universitaire et de traitement de données | Plus adapté aux industries généralistes |
Comme vous pouvez le constater, chaque paradigme possède ses propres spécificités, et on ne peut pas vraiment dire que l’un est meilleur que l’autre. Chacun a été conçu pour résoudre des problématiques spécifiques. Le but de ce tableau est de vous présenter chacune de leur singularité afin que vous sachez comment les exploiter, car les 2 paradigmes sont en général utilisés de façon parallèle lors du développement d’applications Big Data.
Quelques langages de programmation fonctionnelle
Depuis la création de Lips, plusieurs langages fonctionnels ont émergé :
- ML : un langage fonctionnel impur développé dans les années 1970 ;
- OCaml : une implémentation du langage Caml créé en 1996, il fait partie de la famille ML ;
- F# : un langage fonctionnel de la famille ML également, développé par Microsoft Research en se basant sur OCaml ;
- Haskell : le langage qui est parmi les plus utilisés dans la programmation fonctionnelle ;
- Clojure : un langage compilé sur JVM très proche de Lips, elle permet donc l’utilisation des codes Java ;
- Erlang : le langage utilisé principalement dans les industries de pointe comme Google ou Amazon ;
- Scala : un langage également compilé sur JVM, utilisé dans de nombreux domaines, notamment celui du Big Data. Nous allons d’ailleurs le voir plus en détail tout à l’heure.
Toutefois, il existe quelques langages tels que Ruby, Perl, Dylan, VB.Net ou même C# qui autorise l’utilisation de la programmation fonctionnelle au sein de leur code.
Quelques cas d’usage de la programmation fonctionnelle
La programmation fonctionnelle a de nombreux cas d’usage. Nous allons en voir quelques uns ici.
Cas d’usage #1 : le Big Data
Pour les grandes entreprises qui traitent des millions de données par seconde tels que Google, Facebook, Twitter ou encore Netflix, le paradigme fonctionnel est très utile. En effet, comme nous l’avons dit précédemment, lorsqu’on traite les données qui sont accédées de façon concurrente, la mutabilité peut avoir des conséquences négatives irréversibles. La programmation fonctionnelle apporte la solution à cela en permettant aux programmes et aux traitements d’être distribués dans chacun des cœurs d’un serveur multicœur.
Prenons le cas de l’algorithme MapReduce, qui est l’exemple par excellence de ce paradigme. Le principe consiste à diviser les données en bloc de données, à appliquer une fonction sur chacun de ces blocs et par la suite obtenir un résultat final. Les partitions de données sont distribuées dans des clusters pour que leurs traitements soient effectués.
4 étapes sont appliquées lors de ce processus : le Splitting, le Map, le Shuffing et le Reduce. Chacune de ces étapes est exécutée au sein d’une fonction différente. Les valeurs que l’utilisateur de l’algorithme doit fournir sont, un couple (clé,valeur) et les fonctions split, map et reduce.
Pour plus de détails sur le mapreduce, cliquez sur le mot.
Cas d’usage #2 : le parsing
Un autre cas d’usage très fréquent de la programmation fonctionnelle est le parsing. Tous les langages compilés utilisent un parseur. C’est lui qui va transformer les scripts de code en instructions machine.
La meilleure approche lorsque vous souhaitez développer un parseur est d’utiliser la programmation fonctionnelle. Grâce à l’immutabilité des variables, vous éviterez ainsi les bugs qui pourront être fatals pour votre programme. L’immutabilité est très importante dans ce genre de programme, car elle limite l’apparition des effets de bord entrainant les bugs.
Pratique : programmation fonctionnelle avec Scala
Il est temps maintenant de voir en pratique ce que les concepts de la programmation fonctionnelle donnent. Nous allons utiliser scala en guise d’exemple. Notez que cette partie ne traite pas spécifiquement de scala. Le langage est juste utilisé à titre illustratif. Si vous souhaitez apprendre à programmer en scala, nous avons un tutoriel complet que vous pouvez retrouver ici : apprendre à programmer en scala par la pratique.
La déclaration d’une variable immutable
Scala intègre directement la notion d’immutabilité dans la déclaration de ses variables. Cette déclaration se fait par le préfixe « val » suivi du nom de la variable. L’écriture du code est comme ceci :
Val a : String = "toto"
L’évaluation « lazy »
La pratique du « lazy evaluation » ou évaluation paresseuse fait partie des principes fondamentaux de la programmation fonctionnelle. En Scala, la compilation d’une fonction ne se fait que lors de son exécution. Celà libère considérablement la mémoire et améliore la performance du programme. Voici un exemple :
val a = Source.fromFile(« log »).getLines().toList.filter(_.contains("[error] ")).take(5)
Comme vous pouvez le constater, c’est une succession d’appels de fonction au sein d’une même écriture. Le résultat de ces appels seront par la suite retournés dans la variable a. C’est pratique, car en une seule ligne, vous avez pu effectuer toutes les opérations voulues pour obtenir la valeur souhaitée.
Les fonctions pures en Scala
Vu que c’est un langage fonctionnel, il intègre donc la notion de fonction pure. Plusieurs fonctions pures sont présentes dans Scala. Les fonctions de la bibliothèque scala.math._package telle que min, max et abs sont pures.
Certaines méthodes de traitement les chaines de caractères sont aussi pures. C’est le cas de length, substring et isEmpty. D’autres fonctions pures comme map() et reduce() existent aussi.
Les fonctions d’ordre supérieur en Scala
En programmation fonctionnelle, l’utilisation des fonctions d’ordre supérieur fait partie des principes fondamentaux. En Scala, ces fonctions sont considérées comme des valeurs d’ordre supérieur. Elles permettent entre autre d’avoir plus de fluidité dans le code. Voici un exemple :
def sum(f : Int => Int, a : Int, b : Int) : Int = if (a>b) 0 else f(a) + sum (f, a+1, b)
Ici, la fonction sum est une fonction qui a comme paramètre une autre fonction. Ce qui fait d’elle une fonction d’ordre supérieur. Dans la même lignée, une fonction qui fait la somme d’un carré peut alors s’écrire comme suit :
def carre (x : Int) : Int = x*x
def sommecarre (a : Int, b : Int) : Int = sum(square, a, b)
Les fonctions anonymes
Vu que les fonctions ne changent pas d’état, la création de fonctions qui ne seront utilisée qu’une fois encombre les lignes de code. C’est pour cela qu’en Scala, on utilise des fonctions anonymes. Par exemple, une fonction qui va faire une addition va s’écrire comme suit :
(a : Int, b : Int) => a + b
Vous pouvez constater que le code est plus aéré par rapport à une déclaration avec def par exemple.
Voilà ! Si vous êtes arrivé au bout de ce tutoriel. Vous avez maintenant toutes les bases nécessaires pour développer des applications selon le paradigme fonctionnel, et ce quel que soit votre niveau de connaissance initial en programmation, débutant ou confirmé. Maintenant, nous vous invitons à télécharger 1h de formation gratuite sur la programmation fonctionnelle avec Scala afin de mettre vous-même tout ce que vous avez appris en pratique.
« Le premier langage fonctionnel, Lips, a vu le jour en 1958 ! »
Il s’agit de « LISP » (List processing..)
Bonjour Ahmed,
merci pour ta précision, elle est appréciée !!
Juvénal
« Dans ce cas si b=1, f (1, 1) = 2. Et si l’on exécutait une deuxième fois f (1, 1), le résultat devient alors 3. »
Vous êtes sûr de ça ? f(1,1) vaudra 2, même si on l’exécute 10 fois d’affilée.
C’est f(a,1) qui vaudra d’abord 2, puis 3, puis 4 …