La programmation fonctionnelle est née avec le langage LISP (List Processing) créé par John Mac Carthy en 1958. C'est une mise en oeuvre de ce qu'on appelle le lambda-calcul, une théorie inventée par Alonzo Church dans les années 1930 dont l'idée majeure est que tout programme informatique ou tout calcul peut s'écrire ou s'exprimer uniquement avec des fonctions.
Le langage LISP a eu ensuite de nombreux descendants dont Common Lisp, Scheme, Haskell, Caml puis son extension objet OcamL (qui est un langage enseigné actuellement en classe préparatoire MP2I).
Les idées introduites par ces langages ont été reprises dans la plupart des langages modernes qui offrent la possibilité, entre autres, de programmer en suivant un paradigme fonctionnel. En particulier, on verra qu'il est possible d'écrire en Python dans un style fonctionnel.
En Lisp, les listes se notent avec des parenthèses et le traitement de listes repose sur 2 primitives car
et cdr
pour accéder au premier élément et au reste d'une liste. Par exemple, en Lisp, le code suivant définie une fonction récursive qui renvoie le dernier élément d'une liste.
(de dernier (l)
(cond ((null (cdr l)) (car l))
(t (dernier (cdr l)))
)
)
Pour appeler la fonction sur la liste formée des nombres 2, 4, 3 et 9 on écrirait (dernier '(2 4 3 9))
qui renverrait la valeur 9
.
On remarque la notation préfixée des opérateurs et l'usage abondant des parenthèses qui servent à la fois à structurer les données et les programmes.
Si vous souhaitez avoir une explication détaillée du code de cette fonction, lisez ce qu'en dit ChatGPT via Bing Copilot : https://copilot.microsoft.com/sl/kGV04eBTleu.
À condition de définir au préalable les fonctions primitives, on peut, en Python, définir la fonction dernier
, selon le paradigme fonctionnel.
# Fonctions primitives
def listevide():
return []
def premier(L):
return L[0]
def reste(L):
return L[1:]
On a déjà écrit une version impérative de la fonction dernier
:
def dernier(L):
"""Version impérative."""
while reste(L) != listevide(): # tant que le reste de la liste n'est pas vide
L = reste(L) # on passe au reste
return premier(L) # on renvoie le premier élément de la dernière paire
Une version fonctionnelle de cette fonction peut s'écrire ainsi :
def dernier(L):
"""Version fonctionnelle."""
return premier(L) if reste(L) == listevide() else dernier(reste(L))
>>> dernier([2, 4, 3, 9])
9
Pour calculer le pgcd de deux nombres entiers positifs donnés, on peut, dans un style impératif, écrire une boucle qui soustrait alternativement le plus petit du plus grand jusqu'à ce qu'on obtienne 0.
def pgcd(a, b):
"""Version impérative."""
while a != b:
if a > b:
a = a - b
else:
b = b - a
return a
>>> pgcd(56, 184)
8
Dans un style fonctionnel, on écrit une fonction qui renvoie le résultat en précisant la valeur dans les cas simples et en se ramenant, le cas échéant, par appel récursif, à des cas plus simples.
def pgcd(a, b):
"""Version fonctionnelle."""
return a if b == 0 else pgcd(b, a) if b > a else pgcd(a-b, b)
>>> pgcd(56, 184)
8
L'usage de toutes ou une partie de quatre notions suivantes est caractéristique de la programmation fonctionnelle :
dernier(reste(L))
applique la fonction dernier
au résultat de l'application de la fonction reste
à la liste L
),exp1 if cond else exp2
,Par contre, on n'a pas utilisé :
Ces notions sont, quant à elles, les briques de base de la programmation impérative.
Correspondances :
Dans un langage fonctionnel, une fonction est une expression comme une autre, qui peut donc être passée en paramètre à une autre fonction, ou être renvoyée comme résultat.
Une fonction d'ordre supérieur est une fonction qui prend une ou plusieurs fonctions en paramètre, ou qui renvoie une fonction (ou les deux).
Fondamentalement, cela permet de considérer un programme comme une donnée et donc de fabriquer des programmes qui fabriquent ou testent ou vérifient d'autres programmes.
On a déjà vu une tel usage, avec la fonction sorted
qui peut prendre un paramètre - nommé key
- une fonction calculant la clé sur laquelle effectuer le tri.
>>> help(sorted)
Help on built-in function sorted in module builtins:
sorted(iterable, /, *, key=None, reverse=False)
Return a new list containing all items from the iterable in ascending order.
A custom key function can be supplied to customize the sort order, and the
reverse flag can be set to request the result in descending order.
Pour comparer deux éléments, ce sont leurs clés qui sont comparées.
>>> animaux = ['veau', 'vache', 'cochon', 'anaconda', 'chat', 'chien', 'ver',
'poule', 'souris']
>>> sorted(animaux)
['anaconda', 'chat', 'chien', 'cochon', 'poule', 'souris', 'vache', 'veau', 'ver']
On peut utiliser la fonction len
pour trier les chaines selon leur longueur.
>>> sorted(animaux, key=len)
['ver', 'veau', 'chat', 'vache', 'chien', 'poule', 'cochon', 'souris', 'anaconda']
Dans ce dernier exemple, la fonction
len
est passée en paramètre à la fonctionsorted
.
Si on souhaite, on peut trier la liste selon la deuxième lettre de chaque mot.
def deuxieme_lettre(mot):
return mot[1]
>>> sorted(animaux, key=deuxieme_lettre)
['vache', 'veau', 'ver', 'chat', 'chien', 'anaconda', 'cochon', 'poule', 'souris']
lambda
La notation lambda
permet de définir une fonction, puis être nommée - ou pas. On utilise pour cela la syntaxe suivante :
lambda x: 2*x
qui désigne une fonction prenant en paramètre un nombre x
et renvoyant le double de x
.
>>> type(lambda x: 2*x)
<class 'function'>
On peut la nommer si nécessaire, puis l'utiliser, de la façon suivante :
>>> double = lambda x: 2*x
>>> double(16)
32
>>> addition = lambda x, y: x + y
>>> addition(2, 3)
5
Souvent, on va utiliser la notation lambda
pour définir une fonction anonyme, c'est-à-dire sans avoir besoin de la nommer. Par exemple, une telle fonction anonyme peut aussi être fournie comme clé à la fonction sorted
(ou toute autre fonction ayant pour paramètre une fonction).
Dans cet exemple, on trie des points selon leur distance de Manhattan au point (0,0) :
>>> points = [(2, 4), (3, 5), (1, 1), (7,5), (3,1), (5,0)]
>>> sorted(points, key = lambda p: p[0] + p[1])
[(1, 1), (3, 1), (5, 0), (2, 4), (3, 5), (7, 5)]
map
, filter
et reduce
Les fonctions map
, filter
et reduce
sont des fonctions d'ordre supérieur communes en programmation fonctionnelle. Elle sont aussi présentes en Python, via la distribution standard ou des modules classiques.
map
La fonction map
permet d'appliquer la même fonction à tous les éléments d'une liste et de construire la liste des résultats. C'est une fonction qui prend donc en paramètres :
>>> map(lambda x: 2*x, [1, 2, 3, 4, 5])
<map at 0x23ec3a54a48>
Cette fonction renvoie un itérateur auquel on peut appliquer la fonction list
pour calculer effectivement la liste des valeurs sucessives :
>>> list(map(lambda x: 2*x, [1, 2, 3, 4, 5]))
[2, 4, 6, 8, 10]
>>> list(map(lambda x: 2**x, range(10)))
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
>>> list(map(len, ['veau', 'vache', 'cochon', 'anaconda', 'ver']))
[4, 5, 6, 8, 3]
filter
La fonction filter
permet de tester un prédicat sur tous les éléments d'une liste et de construire la liste des éléments le vérifiant. C'est une fonction qui prend donc en paramètres :
On peut construire la liste des éléments pour lesquels le prédicat est vrai en appliquant la fonction list
après filter
:
def est_pair(n):
return n % 2 == 0
>>> list(filter(est_pair, range(20)))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> list(filter(lambda c: len(c) <= 4, ['veau', 'vache', 'cochon', 'anaconda', 'ver']))
['veau', 'ver']
On peut bien sûr combiner les fonctions map
et filter
:
# on construit la liste des carrés pairs des entiers compris entre 0 et 5
>>> (filter(est_pair, map(lambda x: x**2, range(6))))
[0, 4, 16]
Néanmoins, l'usage de map
et de filter
peut être évité en Python grâce à l'écriture par compréhension qui effectue le même calcul en construisant la liste des résultats :
>>> [x**2 for x in range(6) if est_pair(x)]
[0, 4, 16]
reduce
Cette fonction est accessible via le module functools
(documentation : https://docs.python.org/fr/3/library/functools.html).
from functools import reduce
La fonction reduce
permet de généraliser des opérateurs à tous les éléments d'une liste.
Elle prend en paramètres :
f
possédant exactement deux paramètresLa fonction reduce
va alors appliquer la fonction f
aux éléments de la liste, deux à deux et de façon cumulative jusqu'à ce que tous les éléments aient été visités.
Concrètement, reduce(f, [1, 2, 3, 4, 5]))
va calculer f(f(f(f(1, 2), 3), 4), 5)
. C'est donc une fonction qui permet de généraliser la somme, le produit, etc.
>>> reduce(lambda x, y: x + y, range(1, 11)) # somme des entiers de 1 à 10
55
>>> reduce(lambda x, y: x * y, range(1, 11)) # produit des entiers de 1 à 10
3628800
Voici un exemple de programme combinant toutes ces fonctions. L'exemple est inspiré du problème 1 du projet Euler :
Si on liste tous les entiers naturels inférieurs à 10 qui sont multiples de 3 ou de 5, on obtient 3, 5, 6 and 9. La somme des carrés de ces multiples est 151.
Le programme ci-dessous donne la réponse en une seule ligne :
>>> reduce(lambda m, n: m+n, map(lambda n: n*n, filter(lambda n: n%3 == 0 or n%5 == 0, range(10))))
151
Analysez bien cette ligne pour la comprendre. Une version itérative pour trouver la réponse aurait été bien plus longue à écrire.
Une autre caractéristique importante de la programme fonctionnelle s'appelle la transparence référentielle. Il s'agit d'un principe qui prévoit que lorsqu'on applique plusieurs fois la même fonction aux mêmes arguments, alors on obtient à chaque fois le même résultat.
Ce n'est pas toujours le cas, principalement lorsque notre fonction manipule des objets mutables :
def ajouter(liste, x):
"Version impérative"
liste.append(x)
return liste
Si on applique cette fonction sur la liste
>>> une_liste = [0, 1, 2, 3, 4]
elle ne renvoie pas toujours le même résultat, même avec des expressions identiques :
>>> ajouter(une_liste, 5)
[0, 1, 2, 3, 4, 5]
>>> ajouter(une_liste, 5)
[0, 1, 2, 3, 4, 5, 5]
Comme on le peut le voir, notre fonction ajouter
ne respecte pas la transparence référentielle.
Par ailleurs, comme elle modifie un élément externe à la fonction (la liste liste
passée en paramètre), on dit qu'elle possède un effet de bord.
>>> une_liste = [0, 1, 2, 3, 4]
>>> ajouter(une_liste, 5)
>>> une_liste # est modifiée par la fonction
[0, 1, 2, 3, 4, 5]
La programmation fonctionnelle pure prévoit que les fonctions ne doivent pas avoir d'effets de bord, donc elle décourage l'utilisation de variables mutables. Cela n'empêche en rien d'écrire une fonction qui fait le même travail que précédemment. En effet, au lieu de modifier un objet, il suffit d'en créer un nouveau.
def ajouter(liste, x):
"""Version fonctionnelle."""
return liste[:] + [x] # liste[:] crée une copie de liste (équivalent à liste.copy())
La concaténation de deux listes en Python (opération
+
) construit une nouvelle liste, caractéristique d'une approche fonctionnelle (contrairement à.append()
caractéristique d'une approche impérative).
La nouvelle fonction ne modifie plus la liste passée en paramètre (elle n'a plus d'effet de bord) :
>>> une_liste = [0, 1, 2, 3, 4]
>>> ajouter(une_liste, 5)
>>> une_liste # la liste de départ n'est pas modifiée
[0, 1, 2, 3, 4]
Et donc l'application de la fonction donne toujours le même résultat si on lui passe les mêmes arguments (elle respecte la transparence référentielle) :
>>> une_liste = [0, 1, 2, 3, 4]
>>> ajouter(une_liste, 5)
[0, 1, 2, 3, 4, 5]
>>> ajouter(une_liste, 5)
[0, 1, 2, 3, 4, 5]
Avec cette version fonctionnelle, on peut écrire des programmes équivalents à ceux de la version impérative. Cela suppose simplement de modifier légèrement la façon dont on les écrit. Ainsi, pour ajouter 5 puis 6 à notre liste de départ, au lieu d'écrire
# version impérative
>>> une_liste = [0, 1, 2, 3, 4]
>>> ajouter(une_liste, 5)
>>> ajouter(une_liste, 6)
>>> une_liste
[0, 1, 2, 3, 4, 5, 6]
on écrirait :
# version "fonctionnelle"
>>> une_liste = [0, 1, 2, 3, 4]
>>> une_autre = ajouter(une_liste, 5)
>>> encore_une_autre = ajouter(une_autre, 6)
>>> encore_une_autre
[0, 1, 2, 3, 4, 5, 6]
Encore mieux, pour ne pas utiliser d'affectations mais plutôt la composition de fonctions pour faire des calculs successifs (caractéristique d'une approche fonctionnelle), on écrirait :
>>> ajouter(ajouter([0, 1, 2, 3, 4], 5), 6)
[0, 1, 2, 3, 4, 5, 6]
map
, filter
et reduce
sont des fonctions d'ordre supérieur communes en programmation fonctionnelle. Elles permettent d'itérer, d'effectuer des tests et d'effectuer des calculs, donc d'écrire la plupart des programmes impératifs déjà écrits en NSI, par des évaluations de fonctions.Références
Germain BECKER, Lycée Mounier, ANGERS
Ressource éducative libre distribuée sous Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International
Voir en ligne : info-mounier.fr/terminale_nsi/langages_prog/programmation-fonctionnelle