Introduction à la programmation fonctionnelle Philippe Muller septembre 2004 Philippe Muller Introduction à la programmation fonctionnelle Introduction à la programmation fonctionnelle Objectif du cours: Réfléchir aux principes de base de la programmation et de l’informatique à travers l’apprentissage d’un langage simple mais puissant. Différence entre “programmation” (concept abstrait) “programmation sur machine” (réalisation particulière d’un processus de conception). → relative indépendance du langage vis à vis des problèmes de programmation le choix du langage tient à deux choses: puissance d’expression pour résoudre des problèmes complexes et facilité d’utilisation et d’apprentissage pour se concentrer sur l’essentiel → choix de la programmation fonctionnelle n Philippe Muller Introduction à la programmation fonctionnelle La programmation fonctionnelle ? concept fondamental: l’abstraction traiter une chose complexe en la divisant en choses plus simples et en ignorant les détails. objets de base : la fonction un programme est un ensemble de fonctions traitant des données d’entrée des langages interprétés instructions / interprété au fur et à mesure —> machine 6= langages compilés : instructions / préparation (compilation) / execution avantage/ inconvénients évaluation / débuggage plus souple plus lent (mais on peut toujours compiler) Philippe Muller Introduction à la programmation fonctionnelle Le langage choisi : Camel la programmation fonctionnelle est une famille de langage très proches, dont le plus célèbre est LISP développé au MIT. le successeur de LISP: scheme: plus léger, plus simple et aussi puissant, utilisé pour l’éducation ou le scriptage (ex: Gimp) mais pas de typage des données ; on utilisera donc un autre langage fonctionnel typé: Camel Philippe Muller Introduction à la programmation fonctionnelle A retenir processus de calcul : êtres abstraits mis en œuvre dans les ordinateurs ; données : ou information, objets manipulés par les processus ; programme : contrôle évolution des processus ; langage de programmation : traduit les processus de façon symbolique interprète : traduit les processus écrit dans un langage de programmation. Philippe Muller Introduction à la programmation fonctionnelle Elements de base de la programmation Le langage de programmation : permet de donner des instructions ; constitue un cadre d’organisation des processus de calcul. Pour la programmation on distingue souvent deux sortes ”d’objets”: les données (par ex. les nombres) les procédures (par ex. l’addition de nombres) Philippe Muller Introduction à la programmation fonctionnelle Elements du langage des expressions primitives (nombres, fonctions de base) des moyens de composition pour construire des expressions composees d’expressions primitives des moyens d’abstraction pour nommer et manipuler des objets composés comme un tout, comme si c’était une expression primitive. Philippe Muller Introduction à la programmation fonctionnelle Les expressions l’interprète Camel évalue des expressions > Caml Light version 0.74 #1230 ;; - : int = 1230 la réponse précise le type de la réponse avec sa valeur. # 10 + (3*4);; - : int = 22 +, /, * sont des opérateurs décrivant des procédures primitives, les nombres sont des expressions primitives. Une expression composée est formée à partir d’expressions (primitives ou non) et d’opérateurs, terminée par ; ; operateur 3 |{z} expression z}|{ + (5 ∗ 4) ; ; | {z } Philippe Muller expression Introduction à la programmation fonctionnelle Les types de base d’expressions les entiers les réels les booléens les caractères les chaı̂nes de (int) : les nombres entiers relatifs. (float) : nombres décimaux. 1.67 : le vrai (true) et le faux (false). (char) : ’a’ ’X’ ’&’ caractères (string) “bonjour” Les opérateurs n’acceptent que certains types comme arguments. Par exemple, + ne prend que des entiers : # "toujours" + 29 ;; Entrée interactive: > "toujours" + 29 ;; > ^^^^^^^^^^ Cette expression est de type string, mais est utilisée avec le type int. Philippe Muller Introduction à la programmation fonctionnelle Les définitions Pour manipuler des objets, nécessité de les nommer: l’opérateur let # j # # k let j = 20 ;; : int = 20 30 - j ;; : int = 10 let k = 3* j + 27;; : int = 87 La définition est le moyen de base de l’abstraction. Elle permet : de représenter le résultat d’opérations composées ; de construire des objets complexes par étapes ⇒ développement et test incrémental des programmes. Pour cela il faut pouvoir définir des procédures, moyen d’abstraction plus puissant que le nommage d’un objet, en nommant des opérations composées à partir de paramètres. Philippe Muller Introduction à la programmation fonctionnelle Les fonctions nécessité de manipuler des fonctions, avec paramètres de différentes sortes, en nombre différents → besoin d’abstraire les fonctions ce qui définit une fonction : ses paramètres (appelés variables en maths) et la façon dont on la calcule ; Par ex. carre : x → x*x mais une fonction peut l’être de plusieurs choses: l’énergie cinétique en physique, EC: m v → 1/2mv 2 Pour définir une fonction on spécifie donc les paramètres et le corps: let cinetik m v = 0.5 *. m *. v *. v ;; retourne la fonction cinetik : float -> float -> float = <fun> A l’application d’une fonction, par ex. (cinetik 75.5 25.9), les paramètres formels sont remplacés par les valeurs fournies. Philippe Muller Introduction à la programmation fonctionnelle Les expressions conditionnelles Une expression conditionnelle permet l’évaluation d’une expression différenciée selon une condition (vrai ou fausse). Par exemple la définition de la valeur absolue est une expression conditionnelle : |x| = x si 0 ≤ x −x sinon Elle se traduit en Camel par let abs x = if x >=0 then x else -x ;; Philippe Muller Introduction à la programmation fonctionnelle Conditionnelles (suite) On peut utiliser les opérateurs suivants sur les conditions (booléens): = égalité (opérateur polymorphe) or “ou” logique & “et” logique not négation exercice : x < y < 20 x ≤ 15 Philippe Muller Introduction à la programmation fonctionnelle Exercices 1 Evaluer les expressions suivantes 10 ; ; 5 + 3 + 4;; 9 - 1. ; ; 6 / 2;; let a = 3 ; ; let b = a + 3 ; ; a + (b * a) ; ; a = b;; if a = 4 then 6 else if b = 4 then 6+7+a else 25 ; ; 2 Ecrire une fonction qui calcule la circonférence d’un cercle en fonction de son rayon. Philippe Muller Introduction à la programmation fonctionnelle Processus et procédures : la récursion 1 Ecrire une procédure qui calcule la factorielle : fact(n) = n ∗ (n − 1) ∗ · · · ∗ 3 ∗ 2 ∗ 1 = n ∗ fact(n − 1) 2 Ecrire une procédure qui calcule la suite de Fibonacci: u(0) = 1 u(1) = 1 u(n) = u(n − 1) + u(n − 2) Philippe Muller Introduction à la programmation fonctionnelle A retenir : Le sens d’une procédure est indépendant des noms des paramètres formels: let carre x = x * x ; ; let carre y = y * y ; ; Les noms des paramètres d’une procédure sont locaux au corps de la procédure let assez_bon estimation x = (abs ((carre estimation) - x)) < .0001 ;; let carre x = x * x;; Philippe Muller Introduction à la programmation fonctionnelle Exercices Ecrire des fonctions réalisant les opérations suivantes : 1 calcul de la norme d’un vecteur (2d) donné par ses coordonnées 2 calcul de la moyenne de deux nombres (réels) 3 calcul du maximum de deux entiers 4 vérifier si 3 nombres correspondent aux longueurs des côtés d’un triangle rectangle Philippe Muller Introduction à la programmation fonctionnelle Processus et procédures Pour savoir programmer il faut : connaı̂tre les éléments de base de la programmation avoir l’expérience de la programmation : connaı̂tre les procédures les plus utiles être capable de prévoir le déroulement des acions qui suivra le processus et de diriger ce déroulement par un programme. et maintenant quelques formes typiques de déroulement de processus... Philippe Muller Introduction à la programmation fonctionnelle Exemple : extraction de racine carrée différence entre fonctions mathématiques et fonctions programmées fonction mathématique définit une valeur déterminée par un ou plusieurs paramètres √ ex x = y tel que 0 ≤ y et y 2 = x propriétés Philippe Muller procédures informatiques produit un résultat mais ne dit pas la valeur exacte... réalisation Introduction à la programmation fonctionnelle Exemple : extraction de racine carrée and now for something completely different... la méthode de Newton par approximations successives : √ au départ si y est une approximation de x, une meilleure approximation est donnée par la moyenne de y et x/y . Estimation Quotient x/y Moyenne 1 2 1.5 Exemple pour x = 2 : 1.5 1.333333 1.4167 1.4167 1.4118 1.41412 ... ... 1.41412 Exercice : écrire les procédures qui permettent de calculer la racine avec cette méthode. Philippe Muller Introduction à la programmation fonctionnelle Exercices Ecrire des fonctions réalisant les opérations suivantes : 1 le maximum de trois entiers. 2 la puissance quelconque d’un entier (fonction f (x, n) = x n ) 3 la somme des entiers de 1 à n i=n X ( i) i=1 4 la somme des entiers de p à q i=q X ( i) i=p 5 la somme de deux entiers en utilisant la fonction suivante : let inc n = 1 + n;; Philippe Muller Introduction à la programmation fonctionnelle Retour sur la récursion rec. sémantique : (fact 5) → 5*(fact 4) →→ 5*4*(fact 3) →→→ 5*4*3*(fact 2) ...→ 5*4*3*2*(fact 1) ...→ 5*4*3*2*1*(fact 0) rec. syntaxique seulement : (racine1 (racine1 (racine1 (racine1 1.41412 2.0 2.0 2.0 2.0 1.0) 1.5) 1.4167) 1.41412) puis “remontée” des appels Philippe Muller Introduction à la programmation fonctionnelle Les procédures et les structures de bloc Racine1 : premier exemple de processus défini par un ensemble de procédures mutuellement définies. Décomposition d’un programme en procédures (sous-problèmes): pourrait être arbitraire ? blocs de 10 lignes → non: l’important est que chaque procédure exécute une tâche bien définie on n’est pas obligé de s’occuper de la façon dont la procédure calcule ce qu’on lui demande de calculer: ce qui nous intéresse est le résultat L’utilisateur d’une procédure n’est pas forcément son auteur → il peut la recevoir comme une boı̂te noire venant d’un autre, réalisant une certaine fonction (d’où l’importance de l’explicitation des paramètres, “interface” unique entre la fonction et le programmeur qui l’utilise. Philippe Muller Introduction à la programmation fonctionnelle Les noms locaux on peut définir une liaison de (nom,valeur) dont la portée est le corps de la procédure : Exemple: pour max3 let max3 a b c = let m=(max a b) in (if m>c then m else c);; On peut définir plusieurs liaisons (évaluées indépendamment) : let max3 a b c = let m1=(max a b) and m2=(max b c) in (max m1 m2);; si plusieurs définitions successives sont nécessaires, il faut imbriquer: let a = 3.2/.53.5 in let b = a *. a in ... Philippe Muller Introduction à la programmation fonctionnelle Les procédures et les structures de bloc : noms locaux les noms des paramètres d’une procédure sont locaux au corps de la procédure ; le nom d’un paramètre de fonction est une variable liée (le sens de l’expression ne change pas si on renomme le paramètre). une variable qui n’est pas liée est ... libre l’ensemble des expressions pour lesquelles une liaison définit un nom est appelé la portée de ce nom. les variables liés déclarées comme paramètre d’une procédure ont pour portée le corps de cette procédure. les variables libres ont pour portée le corps du programme. Philippe Muller Introduction à la programmation fonctionnelle Les procédures et les structures de bloc : liaisons locales Sur l’exemple de la racine carrée, le programme est composée de procédures séparées, permettant le contrôle de l’usage des noms. ⇒ inconvénient : seul l’appel à racine est utile vu de l’extérieur (ce qui peut être un problème pour les gros programmes) ⇒ on peut enfermer les sous-procédures avec des liaisons locales. let racine x = let rec ameliore estimation x = (moyenne estimation (x /. estimation)) and assez_bon estimation x = (abs_float ((carre estimation) -. x)) < 0.0001 in let rec racine1 estimation x = if (assez_bon estimation x) then estimation else (racine1 (ameliore estimation x) x) in racine1 1.0 x ;; Philippe Muller Introduction à la programmation fonctionnelle Structure de données composées Jusqu’ici on a vu uniquement des données primitives, des types fournis par le langage. Pour la plupart des problèmes on a besoin de représenter des données plus complexes. De même qu’à partir de procédures simples on peut faire des procédures composées, on peut construire des types. on veut faire une procédure qui donne le min et en même temps le max de deux entiers. Mais une fonction ne renvoie qu’une valeur. → on peut utiliser un type composé de deux valeurs: un doublet. Notation : (2,3) let minmax x y = if x < y then (x,y) else (y,x);; let minmax x y = (min x y), (max x y);; De même on peut utiliser des triplets, des n-uplets... (tuples). Philippe Muller Introduction à la programmation fonctionnelle Utilisation des doublets Comment récuperer une valeur de doublet ? let zorgbl x y = let m = minmax x y in (2 * (fst m) + (snd m));; let zorgbl x y = let (m1,m2) = minmax x y in (2* m1 + m2);; Philippe Muller Introduction à la programmation fonctionnelle Intérêt regrouper des données liées intrinsèquement dans un seul objet. Exemple : les vecteurs. Si on écrit une somme de 2 vecteurs (à deux dimensions): let som_vect x1 y1 x2 y2 = (x1+x2),(y1+y2) ;; Plus structuré (plus abstrait): let som_vect v1 v2 = (fst v1)+(fst v2), (snd v1)+(snd v2);; Philippe Muller Introduction à la programmation fonctionnelle Les Listes Si on a besoin d’un nombre quelconque (et qui peut évoluer) de données similaires, on ne peut se contenter de tuples (ex: dictionnaire de mots). Pour cela on peut utiliser des listes: une liste est un ensemble de données de même type. on note une liste [ donnee1 ; donnee2 ; ... ; donneep ] [ 2 ; 19 ; 16 ; 23 ; 20 ] La liste vide est notée [] On peut extraire des éléments d’une liste grâce aux deux opérations suivantes, hd(head), tl(tail): hd [ 23 ; 45 ; 78 ] ; ; retourne 23 tl [ 23 ; 45 ; 78 ] ; ; retourne [ 45 ; 78 ] On peut construire une liste avec les opérateurs suivants : 23::[ 45 ; 67 ] retourne [ 23 ; 45 ; 67] [96 ; 23]@[ 45 ; 67 ] retourne [ 96 ; 23 ; 45 ; 67] Philippe Muller Introduction à la programmation fonctionnelle Les Listes La liste est un type récursif: une liste est <un élément>::<une liste> → la plupart des fonctions de listes peuvent etre exprimés de façon récursive. Ex: compter les éléments d’une liste : let rec compter une_liste = if une_liste=[] then 0 else (1 + compter (tl une_liste));; Philippe Muller Introduction à la programmation fonctionnelle Exercices 1 2 3 comment accéder au 3e élément de [1 ; 2 ; 3 ; 4 ; 5] comment accéder à l’élément 7 de [ [1 ; 2 ; 3] ; [2 ; 7] ] construire les listes avec :: et @ 1 2 3 4 [1 ;2] ajouté devant l [1 ;2] ajouté à l1 et [3 ;4] l et 1 à la fin une liste avec 1 ajouté devant l1 et 3 devant l2 Philippe Muller Introduction à la programmation fonctionnelle Encore des exercices 1 2 3 4 Ecrire une fonction qui retourne le maximum d’une liste d’entiers. Ecrire une fonction qui retourne la somme des éléments d’une liste Ecrire une liste qui retourne la liste des carrés des éléments d’une liste entière. Ecrire la fonction map qui retourne la liste des éléments d’une liste auxquels on a appliqué une fonction f. ex: (map [1 ; 2 ; 3] carre) → [1 ; 4 ; 9] Philippe Muller Introduction à la programmation fonctionnelle Encore des exercices 1 2 3 4 5 dernier l enlever 1 x l enlever tous x l append → concaténation (la refaire!) reverse (inversion) Philippe Muller Introduction à la programmation fonctionnelle Fonctions d’ordre supérieur On a pu remarquer que des procédures différentes se ressemblent dans leur structure, par exemple : let rec somme_n n = if n=0 then 0 else n+ somme_n (n-1);; let rec somme_carres n = if n=0 then 0 else n*n+ somme_carres (n-1);; On peut abstraire encore plus ces opérations en prenantPcomme n=1 paramètre la fonction utilisée à chaque fois. (calculant n=p f (n)). Pour cela on peut avoir des fonctions comme paramètres : let rec somme f n = if n=0 then (f 0) else (f n)+ (somme f (n-1));; Philippe Muller Introduction à la programmation fonctionnelle Utilisation Comment alors désigner la fonction utilisée dans le cas de somme n et somme carres ? On pourrait redéfinir une fonction identité (et une fonction carré), mais on peut aussi désigner la fonction par sa définition en la notant : fun x -> x (et fun x -> x*x) D’où : let somme_n n = somme (fun x -> x) n;; let somme_carre n = somme (fun x -> x*x) n;; Si on revient sur le type de somme : (int -> int) -> int -> int = <fun> On voit qu’en appliquant un paramètre à somme on a une expression de type int -> int (une fonction !). On pourrait donc écrire encore plus simplement : let somme_n = somme (fun x -> x) ;; (Quel est son type ?) ...on peut donc aussi avoir des fonctions qui ont pour résultat une fonction. Philippe Muller Introduction à la programmation fonctionnelle Exercices écrire des fonctions calculant 1 la fonction dérivée (approchée) d’une fonction réelle, sachant que : f 0 (x0 ) = lim x→x0 2 3 4 f (x) − f (x0 ) f (x0 + e) − f (x0 ) = lim e→0 x − x0 e la composition de deux fonctions (f o g). L’appliquer à: f n , et la dérivée de (f o g). le zéro d’une fonction monotone par dichotomie. écrire une fonction filtre qui ne garde d’une liste que les éléments qui vérifient une condition passée en paramètre. Philippe Muller Introduction à la programmation fonctionnelle Encore plus écrire une fonction qui effectue un parcours de liste en effectuant une même opération sur tous les éléments ; l’appliquer pour faire des fonctions qui respectivement: fait la somme des éléments d’une liste calcule la longueur d’une liste teste si qqchose appartient à une liste ajoute un élément à la fin d’une liste refait la fonction map. refait la fonction filtre renverse une liste Philippe Muller Introduction à la programmation fonctionnelle Solutions let rec parcours f el_neutre l = if l=[] then el_neutre else (f (hd l) (parcours f el_neutre (tl l)));; let somme = parcours (fun x y -> (x + y)) 0 ;; let longueur = parcours (fun x y -> (1 + y)) 0 ;; let membre l a = parcours (fun x y -> (x=a) or y) false l ;; let ajoute l a = parcours (fun x y -> x::y) [a] l ;; let remap l f = parcours (fun x y -> (f x)::y) [] l;; let refilter f = parcours (fun x y -> (if (f x) then x::y else y let reverse = parcours (fun x y -> y@[x]) [];; Conclusion : il faut factoriser ! Philippe Muller Introduction à la programmation fonctionnelle Le filtrage la définition d’une fonction par x -> .. (= “à x on associe ...”) peut se diviser : let rec truc = fun 0 -> 1 | 1 -> 1 | x -> (truc (x-1)) + (truc (x-2));; Sur des couples : let machin = fun (0,0) -> 0 | (x,0) -> x+1 | (_,1) -> 2 | (x,y) -> y ;; Sur des listes : let rec quoi = fun [a] -> a | (t::q) -> quoi q;; Philippe Muller Introduction à la programmation fonctionnelle Rencontres du septième type Dans certains cas on peut vouloir définir un objet en le représentant avec des types différents. Par exemple un être humain peut être un désigné par son nom où par un numéro (de sécu par exemple). Dans ce cas on peut spécifier ce type de la façon suivante : #type identite = Nom of string | SS of int;; #let j = Nom("jacko");; j : identite = nom "jacko" #let p = SS(109090190);; p : identite = ss 109090190 Et on peut faire un filtrage pour traiter les cas : let afficher = fun (Nom x) -> print_string x | (SS x) -> print_int x ;; Philippe Muller Introduction à la programmation fonctionnelle Encore une somme Imaginons maintenant que l’on veuille construire un type complexe mélangeant des types différents plus compliqués, par exemple des expressions arithmétiques abstraites simples, comme : 2*x + y*x + 3 Comment faire ? Il faut d’abord analyser les expressions : une expression correcte est formée avec un opérateur et deux arguments, qui sont aussi des expressions : (2 * x) + ((y * x) + 3) Faire une liste ? impossible à cause des types différents ...→ le type “somme”. Philippe Muller Introduction à la programmation fonctionnelle type expr = const of int | operation of (int * ?? * int);; type operateur = Plus | Moins | Mult | Div ;; type expr = const of int var of string | op of (int * operateur * int);; type expr = const of float var of string | op of (expr * operateur * expr);; let exemple = op(op(const(2.),Mult,var("x")),Plus,op(.... Exercices : simplification (des zéros et des uns). dérivée symbolique d’une expression par rapport à une variable. Philippe Muller Introduction à la programmation fonctionnelle Construire un nouveau type : les ensembles Quels opérations veut-on faire sur les ensembles ? On va essayer d’isoler des opérations primitives sur les ensembles toutes les operations doivent pouvoir se déduire des fonctions primitives. → l’implantation ne dépendra que des opérations primitives ; en cas de modifications, le reste ne changera pas. Opérations primitives : ens vide est vide ajoute elem un elem autres elems appartient a Philippe Muller Introduction à la programmation fonctionnelle Les ensembles (2) A partir de là, écrire : union intersection est inclus egaux retire elem listes des parties (E = G ∪ a, P(E ) = P(G ) ∪ (P(G ) ⊕ a) Avec {E1 , E2 , ..., En } ⊕ a = {E1 ∪ {a}, E2 ∪ {a}, ..., En ∪ {a}} creer ens, prenant une liste d’éléments et renvoyant l’ensemble les contenants. Philippe Muller Introduction à la programmation fonctionnelle Les ensembles (3) Maintenant, quelles propriétés doivent satisfaire les primitives pour implémenter les ensembles ? union un elem(a) autres elems(a)=a un elem( ajoute elem a ens vide())=a est vide(ens vide)= oui ... Programmer les primitives en se basant sur des listes. Comment améliorer certaines procédures (ajoute un élément, ...) Philippe Muller Introduction à la programmation fonctionnelle Un nouveau type : les arbres binaires tout noeud a deux descendants: gauche et droit arbres d’entiers: toute valeur à G ≤ valeur du noeud < valeur à droite Philippe Muller Introduction à la programmation fonctionnelle Construire un nouveau type : les arbres binaires Arbre binaire : opérations primitives arbre vide arbre est vide arbres egaux fils g fils d creer arbre racine (valeur de la racine) Philippe Muller Introduction à la programmation fonctionnelle Les arbres binaires Arbre binaire: opérations supérieures (dépendent des primitives seulement) somme des valeurs hauteur ajout arbre ajout valeur croissant val (arbre → liste) insere liste int → arbre binaire sous arbre Philippe Muller Introduction à la programmation fonctionnelle The end: Récapitulation des principes de base décomposer le problème isoler les parties ⇒ facile à modifier + facile à réutiliser encore un exemple : modifier “ensemble” avec arbre binaire ens vide / est vide / ajoute elem / un elem / autre elem / appartient comparaison performances ? Philippe Muller Introduction à la programmation fonctionnelle Exo: les arbres de Huffman (préparation au TP) parcours avec liste de 0,1 (codage huffman) 0 = aller à gauche 1 = aller à droite get a [0 ; 1 ; 0 ; 1] —- sous arbre feuille a [0 ; 1 ; 0 ; 1] — fail ou valeur application: codage séquence, arbre binaire est arbre binaire de lettre : - code one - decode one - codage = “hello” → “0101010” - decodage “0101010” → “hello” -puis decodage (1 char, 1 suite) Philippe Muller Introduction à la programmation fonctionnelle