Programmation fonctionnelle avancée Notes de cours Cours 3 Fonctions sur les listes 19 Septembre 2018 Sylvain Conchon sylvain.conchon@lri.fr 1/43 12/43 2 Définitions de fonctions sur les listes La définition des fonctions sur les listes prennent généralement la forme d une définition à deux cas : le cas où liste est vide le cas où elle ne l est pas Pour cette raison, il est plus agréable de réaliser cette analyse par cas avec du filtrage : let rec f l = [] ->... x::s ->... La fonction zeros La fonction zeros vérifie que tous les éléments d une liste d entiers sont des 0 (renvoie true si la liste est vide) let rec zeros l = [] -> true x::s -> x=0 && zeros s Cette fonction a pour type int list -> bool # zeros [0;0;0];; - : bool = true # zeros [0;1;0];; - : bool = false # zeros [];; - : bool = true 3/43 34/43 4
Recherche d un entier dans une liste La fonction recherche détermine si un entier n figure bien dans une liste l let rec recherche n l = [] -> false x::s -> x=n recherche n s Longueur d une liste La fonction longueur retourne la longueur d une liste let rec longueur l = [] -> 0 _::s -> 1 + (longueur s) Une version récursive terminale : Cette fonction a pour type int list -> bool # recherche 4 [3;2;4;1];; - : bool = true # recherche 4 [1;2];; - : bool = false let longueur l = let rec longrec acc l = [] -> acc _::s -> longrec (1+acc) s in longrec 0 l Cette fonction est prédéfinie en OCAML : List.length 5/43 56/43 6 Fonctions polymorphes (1/2) Quel est le type de la fonction longueur? longueur doit pouvoir s appliquer à des listes d entiers, comme par exemple Polymorphisme # longueur [4;3;6;1;10];; - : int = 5... alors elle doit avoir le type suivant val longueur : int list -> int Mais cette fonction doit aussi pouvoir être appliquée sur une liste dont les éléments sont d un autre type, comme par exemple : # longueur [[4.5;0.3;9.8];[];[3.2;1.8]];; - : int = 3... dans ce cas la fonction longueur devrait également avoir 7/43 78/43 8
Fonctions polymorphes (2/2) Les deux types précédents sont corrects La fonction longueur a une infinité de types Le type inféré par OCAML est le plus général : val longueur : a list -> int a (qui se lit apostrophe a, ou encore alpha ) est une variable de type Une variable de type veut dire n importe quel type Il faut donc lire le type de la fonction longueur comme suit : La fonction longueur prend en argument une liste dont les éléments sont de n importe quel type et retourne un entier Types sommes polymorphes Les type sommes peuvent aussi être polymorphes type a option = None Some of a # None ;; - : a option = None # let v = Some(3);; val v : int option = Some(3) Un autre type somme polymorphe bien connu. type a liste = Vide Cellule of a * a liste # Vide ;; - : a liste = Vide # let l = Cellule(3,Vide);; val l : int liste = Cellule(3,Vide) 9/43 910/43 10 Types sommes polymorphes Les types peuvent également contenir plusieurs variables de type type ( a, b) t = A of a * b B of int Fonctions génériques sur les listes # A(1.2, "r") ;; - : (float, string) t = A(1.2, "r") # A( a, 1) ;; - : (char, int) t = A( a, 1) 11/43 11 12/43 12
Concaténation de listes La fonction append construit une nouvelle la liste en réunissant deux listes bout à bout : let rec append l1 l2 = match l1 with [] -> l2 x::s -> x::(append s l2) Cette fonction a pour type a list -> a list -> a list # append [2;5;1] [10;6;8;15];; - : int list = [2;5;1;10;6;8;15] Cette fonction est prédéfinie en OCAML, il s agit de List.append L opérateur infixe @ est un raccourci syntaxique pour cette fonction, on note l1@l2 la concaténation de l1 et l2 Concaténation rapide La fonction append n est pas récursive terminale Si l ordre des éléments n a pas d importance, on peut définir une concaténation récursive terminale qui inverse les éléments de la première liste let rec rev_append l1 l2 = match l1 with [] -> l2 x :: s -> rev_append s (x :: l2) val rev_append : a list -> a list -> a list = <fun> # rev_append [4;2;6] [1;10;9;5];; - : int list = [6; 2; 4; 1; 10; 9; 5] 13/43 13 14/43 14 Renverser une liste La fonction rev pour renverser une liste l s obtient facilement en concaténant la liste l avec la liste vide [], en utilisant rev append let rev l = rev_append l [] Tri de listes val rev : a list -> a list = <fun> # rev [4;2;6;1];; - : int list = [1; 6; 2; 4] 15/43 15 16/43 16
Tri pas Insertion : principe Cet algorithme de tri suit de manière naturelle la structure récursive des listes Soit l une liste à trier : 1. si l est vide alors elle est déjà triée 2. sinon, l est de la forme x::s et, on trie récursivement la suite s et on obtient une liste triée s on insert x au bon endroit dans s et on obtient une liste triée Insertion La fonction inserer permet d insérer un élément x dans une liste l Si la liste l est triée alors x est inséré au bon endroit On prend pour le moment <= comme relation d ordre let rec inserer x l = [] -> [x] y::s -> if x<=y then x::l else y::(inserer x s) val inserer: a -> a list -> a list # inserer 5 [3;7;10];; - : int list = [3; 5; 7; 10] 17/43 17 18/43 18 Trier une liste Tri Rapide : principe On utilise la fonction inserer pour réaliser un tri par insertion d une liste let rec trier l = [] -> [] x::s -> inserer x (trier s) val trier : a list -> a list = <fun> # trier [6; 1; 9; 4; 3];; - : int list = [1; 3; 4; 6; 9] Soit une liste l à trier : 1. si l est vide alors elle est triée 2. sinon, choisir un élément p de la liste (le premier par exemple) nommé le pivot 3. partager l en deux listes g et d contenant les autres éléments de l qui sont plus petits (resp. plus grands) que la valeur du pivot p 4. trier récursivement g et d, on obtient deux listes g et d triées 5. on renvoie la liste g @[p]@d (qui est bien triée) 19/43 19 20/43 20
Partage d une liste La fonction suivante permet de partager une liste l en deux sous-listes g et d contenant les éléments de l plus petits (resp. plus grands) qu une valeur donnée p let rec partage p l = [] -> ([], []) x::s -> let (g, d) = partage p s in if x <= p then (x::g, d) else (g, x::d) val partage : a -> a list -> a list * a list = <fun> # partage 5 [1;9;7;3;2;4];; - : int list * int list = ([1; 3; 2; 4], [9; 7]) Tri rapide let rec tri_rapide l = [] -> [] p::s -> let (g, d) = partage p s in (tri_rapide g)@[p]@(tri_rapide d) val tri_rapide : a list -> a list = <fun> # tri_rapide [5; 1; 9; 7; 3; 2; 4];; - : int list = [1; 2; 3; 4; 5; 7; 9] 21/43 21 22/43 22 Schémas de définitions récursives Les fonctions suivantes ont toutes la même structure : Itérateurs sur les listes la fonction zeros la fonction recherche la fonction longueur la fonction append la fonction existe la fonction map etc. Laquelle? 23/43 23 24/43 24
Schéma récursif en commun Itération d ordre supérieur Ces fonctions ont toutes le schéma récursif suivant (on note l la liste en entrée et f la fonction définie récursivement) : 1. si l est la liste vide, la valeur retournée par f ne dépend pas de l : c est le cas de base de la récursion ; 2. sinon, l est de la forme x::s et la valeur retournée est calculée en effectuant une opération à partir de x et f s On peut capturer ce schéma à l aide d une fonction d ordre supérieur prenant en argument une fonction f (à deux arguments), une liste l et un élément de départ acc L argument acc représente la valeur retournée pour le cas de base de la récursion La fonction f est appliquée à chaque élément de la liste ainsi qu au résultat de l appel récursif 25/43 25 26/43 26 Exemple : la fonction somme Étape 1 : extraire l opération récursive On montre comment abstraire le schéma d une définition récursive à partir de la fonction somme suivante : let rec somme l = [] -> 0 x::s -> x + (somme s) let rec somme l = [] -> 0 x::s -> (fun a b -> a + b) x (somme s) 27/43 27 28/43 28
Étape 2 : abstraire l opération récursive Étape 4 : abstraire l accumulateur let rec somme fold f l = [] -> 0 x::s -> f x (somme fold f s) let somme l = somme fold (fun a b -> a + b) l let rec somme fold f l acc = [] -> acc x::s -> f x (somme fold f s acc) let somme l = somme fold (fun a b -> a + b) l 0 ou plus simplement let somme l = somme fold (+) l 0 29/43 29 30/43 30 Déroulons tout ça somme [1;2;3] somme fold (+) [1;2;3] 0 (+) 1 (somme fold (+) [2;3] 0) (+) 1 ((+) 2 (somme fold (+) [3] 0)) (+) 1 ((+) 2 ((+) 3 (somme fold (+) [] 0))) (+) 1 ((+) 2 ((+) 3 0)) (+) 1 ((+) 2 3) (+) 1 5 6 L itérateur fold right La fonction somme fold n est pas spécifique au calcul de la somme d une liste d entiers. Son schéma récursif est capturé par la fonction suivante : fold right f [ ] acc = acc fold right f [ e 1 ; e 2 ;... ; e n ] acc = f e 1 (f e 2 ( (f e n acc) )) Cette fonction s appelle List.fold right dans la bibliothèque standard de OCaml : let rec fold_right f l acc = [] -> acc x::l -> f x (fold_right f l acc) Son type est ( a -> b -> b) -> a list -> b -> b Attention : cette fonction n est pas récursive terminale! 31/43 31 32/43 32
L itérateur récursif terminal fold left Pour les fonctions dont l ordre d application de l opération sur x et g s n est pas important, on peut utiliser le parcours suivant : fold left f acc [ ] = acc fold left f acc [ e 1 ; e 2 ;... ; e n ] = f ( (f (f acc e 1 ) e 2 ) ) e n Exemple 1 : somme d une liste d entiers (suite) On peut écrire la fonction somme avec List.fold left let somme l = fold_left (+) 0 l val somme : int list -> int Cette fonction s appelle List.fold left dans la bibliothèque standard de OCaml : let rec fold_left f acc l = [] -> acc x::s -> fold_left f (f acc x) s Son type est ( a -> b -> a) -> a -> b list -> a Cette fonction est récursive terminale! somme [1;2;3] = fold left (+) 0 [1;2;3] fold left (+) ((+) 0 1) [2;3] = fold left (+) 1 [2;3] fold left (+) ((+) 1 2) [3] = fold left (+) 3 [3] fold left (+) ((+) 3 3) [] = fold left (+) 6 [] 6 33/43 33 34/43 34 Exemple 2 : Longueur d une liste Évaluation de la fonction longueur Rappel : version sans itérateur let rec longueur l = [] -> 0 x::s -> 1 + (longueur s) val longueur : a list -> int Version avec itérateur : let longueur l = fold_left (fun acc x -> 1 + acc) 0 l longueur : a list -> int # longueur [3;2;1;4];; - : int = 4 Évaluation de longueur [3;2;1;4] On note plus1 la fonction (fun acc x -> 1 + acc) longueur [3;2;1;4] = fold left plus1 0 [3; 2; 1; 4] fold left plus1 (plus1 0 3) [2; 1; 4] = fold left plus1 1 [2; 1; 4] fold left plus1 (plus1 1 2) [1; 4] = fold left plus1 2 [1; 4] fold left plus1 (plus1 2 1) [1; 4] = fold left plus1 3 [4] fold left plus1 (plus1 3 4) [] = fold left plus1 4 [] = 4 35/43 35 36/43 36
Exemple 3 : concaténation de deux listes Rappel : version sans itérateur let rec append l1 l2 = match l1 with [] -> l2 x::s -> x::(append s l2) append : a list -> a list -> a list Version avec itérateur let append l1 l2 = fold_right (fun x acc -> x::acc) l1 l2 val append : a list -> a list -> a list = <fun> # append [1;2;3] [4;5;6];; - : int list = [1; 2; 3; 4; 5; 6] Autres fonctions let zeros = fold_left (fun acc x -> x=0 && acc) true val zeros : int list -> bool = <fun> let recherche n = fold_left (fun acc x -> x=n acc) false val recherche : a -> a list -> bool = <fun> let existe p = fold_left (fun acc x -> p x acc) false val existe : ( a -> bool) -> a list -> bool = <fun> 37/43 37 38/43 38 Exemple 4 : liste des sous-listes d une liste (1/2) Exemple 4 : liste des sous-listes d une liste (2/2) On souhaite écrire une fonction sous listes pour calculer la liste des sous-listes d une liste l On commence par écrire une fonction cons qui ajoute un élément à toutes les listes d une liste de listes : let rec cons_elt x l = [] -> [] r::s -> (x::r)::(cons_elt x s) cons_elt : a -> a list list -> a list list La fonction sous liste s écrit alors naturellement de la manière suivante : let rec sous_listes l = [] -> [[]] x::s -> let p = sous_listes s in (cons_elt x p)@p sous_listes : a list -> a list list # sous_listes [1;2;3];; - : int list list = [[1; 2; 3]; [1; 2]; [1; 3]; [1]; [2; 3]; [2]; [3]; []] 39/43 39 40/43 40
Exemple 5 : sous liste avec itérateurs On commence par définir l itérateur map de type ( a -> b) -> a list -> b list tel que map f [e 1 ;...; e n ] = [f e 1 ;...; f e n ] let rec map f l = [] -> [] x::s -> let v = f x in v :: (map f s) Itérateurs vs. fonctions récursives (1/2) Les versions sans itérateurs des fonctions zeros, recherche et existe sont plus efficaces que leurs versions avec itérateurs respectives let rec existe p l = [] -> false x::s -> p x (existe p s) On note p la fonction fun x -> x=0 Le fonction sous liste peut alors s écrire (avec @ l opérateur de concaténation en OCaml) : let sous_listes = fold_left (fun p x -> (map (fun l->x::l) p)@p) [[]] existe p [3;0;2;1;4] p 3 existe p [0;2;1;4] = existe p [0;2;1;4] p 0 existe p [2;1;4] = true 41/43 41 42/43 42 Itérateurs vs. fonctions récursives (2/2) La version avec itérateur va jusqu au bout de la liste let f = fun acc x -> p x acc let existe p = fold_left f false existe p [3;0;2;1;4] = fold left f false [0;2;1;4] fold left f (p 3 false) [0;2;1;4] = fold left f false [0;2;1;4] fold left f (p 0 false) [2;1;4] = fold left f true [0;2;1;4] fold left f (p 2 true) [1;4] = fold left f true [1;4] fold left f (p 1 true) [4] = fold left f true [4] fold left f (p 4 true) [] = fold left f true [] 43/43 43