Paradigmes — Le fonctionnel

paradigmes Par GuilOooo, le vendredi 17 avril 2009, à 01:04

Pour ne pas nous noyer sous les objets, dans cet article, nous allons partir à la découverte du paradigme fonctionnel. Il offre un point de vue très riche sur la programmation, et commence à s'éloigner de ce que connait un débutant qui a lu quelques tutoriels sur les langages mainstream. En avant !

On oublie les états

Vous vous souvenez du paradigme impératif ? Nous avions vu que, dans ce paradigme, l'exécution d'un programme était essentiellement une succession de changements d'états. Le paradigme fonctionnel, par opposition, essaie de minimiser ces changements d'états, et par extension de se baser le moins possible sur la notion d'états du programme. Concrètement, cela se manifeste par l'absence d'affectations de variables dans les programmes fonctionnels, et, en conséquence, une absence de boucles. En effet, si vous interdisez la modification de variables, les conditions de vos boucles seront toujours vraies [ou fausses], l'état du programme ne changeant pas.

Cette idée d'arrêter les affectations perturbe pas mal de débutants en fonctionnel. La question que l'on se pose est : « comment vais-je structurer mon programme ? ». Le paradigme a donc développé un panel d'outils destinés à remplacer les affectations et les boucles, principalement basés sur les fonctions (d'où le nom). En effet, plutôt que de construire un programme en prévoyant une suite de changements d'états, le programmeur fonctionnel écrit une suite de fonctions qui agissent comme de petites « boîtes noires » que l'on peut imbriquer les unes dans les autres. Attention aux confusions : ici, le terme « fonction » renvoie bien à la fonction mathématique, et pas à la procédure que l'on a vue dans le chapitre de l'impératif.

La fonction

Ici, une fonction est une entité à part entière, qui, à une entrée, fait correspondre une sortie. Une entrée est constituée d'une ou plusieurs valeurs, comme 42, 13.37, 'z', "blabla" ou encore x -> x². Oui, oui, vous avez bien vu : x -> x², c'est à dire la fonction qui à x fait correspondre x². C'est une valeur parfaitement valide, que l'on peut passer à une fonction, ou encore peut être renvoyée par une fonction. C'est là une originalité de paradigme fonctionnel, et elle peut nous servir à éviter les effets secondaires.

En effet, on peut très bien imaginer une fonction prenant en paramètre quelques données et une autre fonction, puis qui fait des calculs sur ces données et les réinjecte dans la nouvelle fonction en l'appelant. Ainsi, la fonction passée en entrée dirait quoi faire après avoir terminé son calcul. On peut alors écrire un programme qui consisterait en une suite de fonctions exécutées successivement de cette manière. Ce style de programmation s'appelle le continuation-passing style {en}.

Une autre technique, très utilisée en fonctionnel et ailleurs, qui permet de s'affranchir des boucles : la récursivité. Le principe est d'écrire des fonctions qui, dans certains cas, s'appellent elles-mêmes. Le programmeur peut ainsi découper le travail à faire en parties plus petites que l'entrée original, faire faire le travail sur chacune de ces parties par la même fonction, puis combiner les résultats. Le concept est assez abstrait, mais vous pourrez trouver plus d'informations ici, un exemple de mise en oeuvre ici, et encores d'autres éléments sur wikipédia.

Les types

Les langages fonctionnels, comme tous les autres, proposent généralement des systèmes types assez avancés. Ainsi, outre les habituels types pour les nombres (entiers, flottants, etc), les caractères et les chaînes de caractères, on trouve une classe de type bien particuliers : les types algébriques. Les entités appartenant à de tels types peuvent prendre plusieurs formes. Par exemple, on peut définir une liste chaînée comme étant :

  • un élément avec une liste chaînée à la suite,
  • ou bien la liste vide.

Ainsi, la liste vide (notée avec une paire de crochets vide, []) est une liste chaînée, et toute suite d'éléments ayant la bonne structure en est une. On peut noter la mise bout à bout d'un élément « elem » et d'une liste « list » comme ceci elem:list. On a donc 1:2:3:[], 'a':'b':'c':'d':[] et (1:2:3:[]):(4:5:6:[]):[] qui sont des listes (le dernier est même une liste de listes !).

Ces types permettent de faire du filtrage sur les types. Ainsi, on peut créer des fonctions qui associent telle valeur à une entité de telle forme, telle autre valeur à une entité d'une autre forme. Un exemple (en langage Haskell, expliqué ci-après) vaut mieux qu'un long discours :

somme []           = 0
somme (elem:suite) = elem + (somme suite)

Cette série d'équations définit une fonction appelée somme. Si on passe une liste vide à somme, elle renvoie zéro. Si on lui passe une liste contenant des éléments, elle va additionner la première valeur de la liste à la somme du reste. Le tout, bien sûr, renverra la somme des valeurs dans la liste. On peut ainsi « casser » la liste pour en extraire les valeurs, et il en est de même avec tous les types algébriques.

Cependant, la liste originale, que l'on a initialement passée en paramètre à somme, n'est jamais, jamais modifée. On se contente de la regarder sous un angle bien particulier, mais on ne la modifie jamais. D'ailleurs, dans la seconde équation, il n'y a aucune instruction qui modifie elem ou suite : la liste initiale est donc bien conservée. On se contente de l'analyser sans la toucher. Il n'y a donc aucun effet de bord, et on peut quand même calculer la longueur voulue.

Au passage, on remarque que la définition d'une liste s'utilise elle-même. Les types peuvent donc être récursifs, aussi bien que les fonctions. La récursivité avec les types sert notamment à définir les listes (comme on l'a vu), mais aussi les arbres. Il est bien plus naturel de mettre des fonctions récursives en oeuvre avec des types récursifs, ce qui fait qu'on utilise beaucoup cette technique en programmation fonctionnelle. En langage C, manipuler un tableau avec une solution récursive vient moins naturellement à l'esprit qu'en langage OCaml, où on doit manipuler une liste chaînée.

Résumé : exemple de code

Bon, vous avez déjà eu un exemple de code au dessus, mais en voici un autre, plus « concret ». Il s'agit d'un tri rapide en Haskell. Une fois n'est pas coutume, je vais d'abord vous donner le code, et ensuite les explications permettant de comprendre le langage. Vous avez déjà eu un échantillon de Haskell au dessus, alors essayez de trouver comment il fonctionne, en sachant l'algorithme utilisé :

tri []           = []
tri (prem:suite) = tri (filter (< prem) suite) ++ [prem] ++ 
                     (tri (filter (>= prem) suite))

On remarque déjà que le code est extrêmement concis. De plus, il utilise l'ensemble des traits de la programmation fonctionnelle sur lesquels nous nous sommes penchés - récursivité, filtrage, listes chaîneés - et ne fait aucune affectation pour trier la liste.

Analysons cela plus en détails. L'opérateur ++ permet de concaténer deux listes, c'est à dire des les mettre l'une à la suite de l'autre : [1, 2, 3] ++ [4, 5, 6] renverra [1, 2, 3, 4, 5, 6]. Ensuite, la fonction filter prend en paramètre une liste et un prédicat, et renvoie une liste contenant uniquement les éléments de la liste initiale obéissant au prédicat. Ainsi, on a l'égalité :

filter (< 6) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] = [1, 2, 3, 4, 5]

Du coup, la première ligne du code se lit : « trier une liste vide revient à renvoyer une liste vide ». La seconde : « trier une liste commençant par un nombre (appelé prem) et suivi d'une liste (appelée suite) revient à concaténer l'ensemble des éléments de suite inférieurs à prem, que l'on aura triés, avec prem, avec l'ensemble des éléments de suite supérieurs ou égaux à prem, que l'on aura triés ».

Conclusion

Ce paradigme fournit une vision de la programmation radicalement différente des deux premiers (bien que, comme nous pourrons le voir par la suite, l'objet peut très bien se marrier au fonctionnel).

Si vous êtes intéréssés par le sujet, vous pouvez apprendre des langages tels qu'Haskell, OCaml, Lisp (ou une de ses variantes), ou encore Erlang. Ils mélangent tous un peu d'impératif avec le paradigme fonctionnel, dans différentes proportions. On dit de quelque chose de très « fonctionnel », avec pas ou très peu d'effet de bord, qu'il est « pur », et inversement de quelque chose qui fait beaucoup d'effets de bord qu'il est « impur ». On a ainsi des langages purs/impurs, des codes plus ou moins purs... Essayez donc de coder de la manière la plus pure possible !

source

#1 Par Maxibolt, le 19 Avril 2009 à 19h33

Bien expliqué, mais quand on connaît un peu, on reste un peu sur sa faim.

#2 Par GuilOooo, le 20 Avril 2009 à 20h42

Maxibolt, normal, cet article est avant tout déstiné aux débutants. L'idée, c'est de les introduire à tous les concepts chouettes des différents paradigmes, pour pouvoir ensuite faire des supers articles comparatifs où on explique pourquoi notre religion est la meilleure.% Lucas Panny 1281697952

#3 Par Lucas Panny, le vendredi 13 août 2010, à 13:08

Si c'est destiné aux débutants, j'ai rien compris. C'est bizarre qu'après 4 ans d'études en info, on nous a rien dit sur le fonctionnel

Les langages comme C, C++, Pascal, Java sont de quel type donc?

#4 Par monwarez, le samedi 21 août 2010, à 12:08

ces langages sont de types impératif ( C, C++, Pascal, Java)

#5 Par TsLgYzEbEGpC, le mercredi 05 janvier 2011, à 05:01

2Em99i <a href="http://fzaoseioaonw.com/">fzaoseioaonw</a>, [url=http://eksrsmdxumqu.com/]eksrsmdxumqu[/url], [link=http://oozddzbtmapp.com/]oozddzbtmapp[/link], http://pspsengrmupf.com/

Poster un commentaire