mars
2009
Bonjour,
Je souhaitais écrire un billet à propos de la programmation fonctionnelle, ce sera chose faite.
Cela fait maintenant plus de 6 mois que j’apprends et pratique la programmation fonctionnelle, aussi bien sur le point de vue théorique en m’intéressant à ses relations avec la théorie des catégories (mathématiques) entre autres, que sur le point de vue pratique, c’est à dire que j’ai mené quelques projets (de petite envergure… oui, c’est proportionnel au temps que je peux passer dessus) à bien.
Quand on dit « langage fonctionnel » ou « programmation fonctionnelle », les gens s’imaginent (pour ceux qui ne connaissent pas) des programmes avec tout un tas de fonctions, c’est tout. Ce n’est pas ça la programmation fonctionnelle. Enfin biensûr il y a des fonctions, mais la programmation fonctionnelle ne s’arrête pas là.
Déjà, la programmation fonctionnelle, c’est une toute autre façon de concevoir des programmes. Détailler toutes les différences avec la programmation impérative serait pire que les 12 travaux d’Hercules et c’est pourquoi je ne le ferai pas. Je vais toutefois vous parler des points qui font que je suis désormais adepte de ce style de programmation.
Tout d’abord, étant assez orienté mathématiques, il y a une première chose qui me plaît. Dans les langages de programmation fonctionnelle (du moins la plupart), on définit des types, des opérations sur ces types, le tout très simplement et de manière on ne peut plus cohérente.
Ensuite, pour les langages fonctionnels compilés et typés statiquement, on est généralement accompagné d’un compilateur très strict, qui tentera d’éliminer un maximum d’incohérences (comprendre « erreurs ») en nous envoyant des erreurs très précises en rapport avec le fait que nous n’avons pas bien typé des parties de notre programme. De plus, les 3 langages majeurs pour une utilisation « real world », Haskell, OCaml et F#, ont vu leur compilateur se doter de l’inférence de type ; il s’agit de la capacité qu’a un compilateur a donner lui-même un type à vos valeurs dans le programme (fonctions, expressions, …). Entre ceci et la vérification des types, la compilation possède une part de travail autour des types en action dans votre code.
De plus, en programmation fonctionnelle, il s’agit de ne pas avoir ce que l’on appelle des effets de bords ; on parle de programmation fonctionnelle pure ; dans un tel cadre, une fonction à qui l’on donne les mêmes entrées renverra toujours la même sortie. Les programmeurs fonctionnels font en sorte d’isoler les parties pour lesquelles ce n’est pas vrai (lecture d’un fichier, intéraction réseau, …) : monades en Haskell, on laisse la possibilité de faire de l’impératif pour OCaml et F#, …
Continuons sur la curryfication. Cette dernière consiste à faire par exemple d’une fonction de 2 arguments 2 fonctions à 1 argument, qui pourront s’appliquer partiellement. Si l’on définit une fonction « add » pour ajouter 2 nombres x et y, alors on pourra appliquer add à son premier argument, x, en lui donnant par exemple la valeur 12, pour obtenir une fonction à UN SEUL argument qui additionnera son argument à 12.
Les fonctions sont capitales en programmation fonctionnelle. Contrairement à dans un certain nombre de langages (beaucoup), les fonctions peuvent être stockées, passées en argument, construites à la volée, retournées par d’autres fonctions, etc. Elles deviennent des valeurs de première classe.
A propos du passage en argument, une chose très importante en programmation fonctionnelle est la possibilité d’abstraire une partie des algorithmes et d’en confier la responsabilité à une fonction donnée en argument par exemple. Une fonction qui prend une autre fonction en argument est appelée « fonction d’ordre supérieur ». Cela permet de factoriser un maximum de code tout en laissant la variabilité possible grâce au passage de fonction en argument. Le compilateur fera les vérifications nécessaires pour voir si les types concordent, ne vous inquiétez pas.
La paresse est également quelque chose qui s’avère bien sympathique parfois. Il s’agit de n’évaluer une expression qu’au moment où l’on en aura besoin (affichage du résultat de l’évaluation de l’expression — comme une addition — par exemple). Elle est mise en oeuvre différemment selon les langages (implicitement en Haskell, c’est le compilateur qui gère cela, alors que c’est explicit en OCaml — module Lazy).
Tout cela, rajouté aux syntaxes assez simples, donne une très grande expressivité aux langages fonctionnels, proche de l’expressivité que l’on a en mathématiques. Qui plus est, rappelons-le, les langages fonctionnels sont des langages dits « déclaratifs ». On exprime le problème, on obtient la solution, contrairement à ce que l’on fait dans un langage impératif où l’on doit donner toutes les étapes à la main nous-mêmes.
Bref, beaucoup d’avantages, un tout nouveau style de programmation, cela ne vaut-il pas le détour ? Allez, si vous voulez en savoir plus, voici quelques liens essentiels pour terminer ce billet sur la programmation fonctionnelle.
Forums sur les langages fonctionnels
Cours d’introduction à OCaml de Damien Guichard — dont je me suis beaucoup servi et que j’apprécie beaucoup
Traduction de « A gentle introduction to Haskell » — un peu rude pour débuter, mais vous fera comprendre pas mal de choses.
N’hésitez pas à utiliser le forum pour donner vos avis et retours d’expérience ou pour des questions, bien évidemment !
Et en Haskell la forme curryfiée c’est du sucre, non ?
C’était pour répondre à spiceguid, en lui donnant un contre exemple.
C’est bien ce que je disais dans le billet non ?
Non, ça ne fait pas tout à fait la même chose. Remplace la fonction add par la fonction cpt suivante :
let cpt str =
let c = ref 0 in
fun () -> incr c; sprintf « %s_%d » str !c
La curryfication n’est pas que du sucre. Avec ma fonction cpt, la sémantique n’est pas conservée. Avec OCaml, une fonction à deux arguments est vue comme une fonction à un argument, renvoyant une fonction.
@Bruno
Je ne suis pas exactement du même avis que Alp.
La curryfication n’a pas d’importance sémantique, ça n’est qu’une facilité syntaxique. Ce qui a vraiment une grande importance sémantique c’est surtout la capacité à construire des fonctions à la volée.
L’exemple que Alp a en tête est celui-ci :
let plus x y = x + y
Où il peut construire plus12 à l’aide de l’application partielle :
let plus12 = plus 12
C’est bien. Mais la forme curryfiée n’est en rien nécessaire pour obtenir ce résultat.
À la place on pourrait très bien écrire ceci :
let add (x,y) = x + y
let add12 y = add(12,y)
Qui est décurryfié et qui fait excatement la même chose. Par contre, syntaxiquement, il est plus léger d’écrire plus 12 que d’écrire fun y -> add(12,y), mais au final c’est la même sémantique.
Deux questions, deux réponses séparées.
@benwit : cela peut sembler moins adapté que Java, C# et autres pour des applications de gestion. Toutefois, encore une fois cela dépend des affinités, je verrais bien des « higher-order functions » pour travailler sur des données d’applications de gestion. Il y a pleins de petites choses que je trouve tellement essentielles maintenant que je ne comprends pas pourquoi les langages mainstreams ne s’en dotent pas, bien que la plupart intègrent désormais ou bientôt les lambda expressions (fonctions anonymes). La programmation fonctionnelle c’est vraiment le fait de composer des fonctions, avec des outils très puissants à côté (typeclasses en Haskell, modules/foncteurs d’OCaml, à titre d’exemple), qui font qu’exprimer ce que l’on veut faire devient réellement aisé. Qui plus est, dans de tels langages, on résout les problèmes en les modélisant, et non pas en décrivant étape par étape la construction de la solution.
Petit « hors-sujet » : certains programmeurs fonctionnels « s’amusent » même à écrire leurs codes en style « point-free », c’est à dire qu’ils font apparaître le moins possible le nom des arguments de fonction, en n’exprimant les nouvelles définitions de fonctions qu’avec des compositions de fonctions. On y constate très aisément la puissance des fonctions d’ordre supérieur.
Pour conclure sur les applications de gestion, je dirais que oui on peut les faire. On n’a pas toutes les facilités qu’on a en Java avec des frameworks mûrs et robustes, des IDE qui font notre boulot, mais on a, à mon sens, d’autres outils qui rendent la programmation plus facile, efficace, sûre et agréable. Si vous voulez vraiment regarder la programmation fonctionnelle pour des applications de gestion, le mieux est surement F# qui donne un accès total au framework .NET, ou bien Haskell qui a une communité très efficace qui produit énormément de bibliothèques. J’ai toutefois, il me semble, croisé des modules pour OCaml pour l’intéraction avec des BDD. Par contre, aux oubliettes les outils de mapping relationnel-objet. Il s’agirait plus, en programmation fonctionnelle, de composer des fonctions de traitements et de les passer à des fonctions d’ordre supérieur pour la suite.
Sinon, les projets que j’ai réalisés avec Haskell et OCaml… Hmm. J’ai commencé une contribution à OCaml Batteries Included (un effort de mise en commun et d’écriture de code pour en faire une bibliothèque standard pour OCaml : http://batteries.forge.ocamlcore.org/), c’est mon seul projet-futur sérieux en OCaml. Sinon sur mon profil DVP, tu pourras voir un screen de mon programme OCaml qui trace l’ensemble de Cantor par itération successive (au sens mathématique, je n’utilise pas for !). En Haskell, j’ai écrit un petit truc qui compile et exécute du code OCaml à la volée, sur demande, avec comme options d’afficher les types inférés pour les valeurs/fonctions définies dans le code, le tout communiquant par le réseau (IRC, dans mon cas). C’est un « bot » à qui tu donnes du code et qui te renvoie donc soit le résultat de l’exécution, soit les types inférés, soit le temps d’exécution, et permet de chercher les types de valeurs/fonctions définies dans l’environnement OCaml du bot (bibliothèque standard OCaml + OCaml Batteries Included en l’occurence). Pas eu le temps de faire quelque chose de plus utile et sérieux pour le moment, mais ça va venir !
@bruno : Oui, vraiment. Mon exemple pour l’addition est tout à fait… débile ? C’était pour l’illustration simple. Je vais tenter d’illustrer sur un projet « real world ». Imaginons que tu ais créé un type pour représenter un ensemble de données métier que ton application doit gérer (informations sur le marché financier, sur le personnel d’une entreprise, … — chose que tu ferais avec une classe dans un langage comme Java, C#, C++ ou autre). Imaginons que tu veuilles maintenant afficher chaque « enregistrement » (un « row » dans une table de base de données, quoi) dans un contrôle graphique de type ListBox là, bref un truc qui correspond à la vision « row » de BdD.
Là, on va donc a priori avoir une fonction addToListBox, par exemple, qui prend un ListBox ainsi qu’un « enregistrement » en argument, et qui va ajouter l’enregistrement à ton ListBox. Or, dans ton programme, tu n’ajouteras des enregistrements qu’à ton ListBox.
Ta fonction addToListBox, si elle est curryfiée, te permettra d’en tirer une nouvelle, qui elle ne prendra qu’un seul argument, le record, et ajoutera au ListBox donné. Quelque chose comme :
addRecordToMyListBox = addToListBox monListBox
Tu appliques donc addToListBox à l’argument monListBox. Ceci te retourne une fonction à un seul argument, un enregistrement, et tu donnes un nom à cette fonction : addRecordToMyListBox.
Tu n’auras ensuite plus qu’à appeler « addRecordToMyListBox » sur chacun des enregistrements. Si tu utilises une bonne structure de données pour stocker les enregistrements, cela se fait avec un code proche de :
iterate addRecordToMyListBox myRecord{Set/List/Anything}
iterate est ici justement une fonction d’ordre supérieur, qui prend en premier argument la fonction à appeler sur chaque élément.
Ceci est un exemple trivial, mais plus attaché à une application de tous les jours. Je t’invite à lire un peu plus sur la programmation fonctionnelle, si ça t’intéresse (tout comme toi, benwit, et en fait tout le monde), afin de te faire ton propre avis sur tout ça. Et n’hésitez pas à venir discuter sur le forum langages fonctionnels.
Par contre, vous serez prévenus, après de nombreuses années de programmation impérative, ça peut être légèrement déroutant. Il faut un petit temps d’adaptation, dirons-nous.
Merci pour votre intérêt pour ce billet en tout cas.
Salut Alp,
pourquoi la curryfication est-elle un concept si important ?
Bruno
Salut Alp,
Ce style de programmation est sûrement adaptable à de nombreux cas (personnellement, j’aime bien le lisp) mais n’est à mon avis pas adapté pour les applications de gestion.
Les projets dont tu parles, c’est de quel genre ?
@+