Programmation Fonctionnelle Stefano Guerrini [email protected] http://www.lipn.univ-paris13.fr/~guerrini Introduction LIPN - Institut Galilée, Université Paris Nord 13 Licence Info 3 février 2013 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 1 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Introduction à la programmation fonctionnelle et à OCaml février 2013 2 / 131 Introduction Styles de programmation Style applicatif I I I Introduction à la programmation fonctionnelle en OCAML I Fondé sur l’évaluation d’expressions, où le résultat ne dépend que de la valeurs des arguments (et non de l’état de la mémoire). Donne des programmes courts, faciles à comprendre. Usage intensif de la récursion. Langages typiques : Lisp, ML, Caml (Camlight, Ocaml). Style impératif I I I I Fondé sur l’exécution d’instructions modifiant l’état de la mémoire. Utilise une structure de contrôle et des structures de données. Usage intensif de l’itération. Langages typiques : Fortran, C, Pascal. Style objet I I I I S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 3 / 131 Un programme est vu comme une communauté de composants autonomes (objets) disposant de ses ressources et de ses moyens d’interaction. Utilise des classes pour décrire les structures et leur comportement. Usage intensif de l’échange de message (métaphore). Langages typiques : Simula, Smalltalk, C++, Java. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 4 / 131 Introduction à la programmation fonctionnelle et à OCaml Introduction Introduction à la programmation fonctionnelle et à OCaml Introduction à OCaml La petite histoire Domaine d’utilisation du langage OCaml OCaml (Objective Caml) est le fruit de développements continus : Un langage d’usage général avec des domaines de prédilection. 1975 Robin Milner propose ML comme métalangage (langage de script) pour l’assistant de preuve LCF. Il devient rapidement un langage de programmation à part entière. 1985 Développement de Caml à l’INRIA et en parallèle, de Standard ML à Édimbourg, de “SML of New-Jersey”, de Lazy ML à Chalmers, de Haskell à Glasgow, etc. 1996 Objets et classes (OCaml) I 2001 Arguments étiquetés et optionnels, variantes. I 2002 Méthodes polymorphes, librairies partagées, etc. I I 2003 Modules récursifs, private types, etc. février 2013 5 / 131 Unison MLdonkey (peer-to-peer) Libre cours (Site WEB) Lindows (outils système pour une distribution de Linux) S. Guerrini (LIPN - Paris 13) Introduction à OCaml Programmation Fonctionnelle Introduction à la programmation fonctionnelle et à OCaml Quelques mots clés février 2013 6 / 131 Introduction à OCaml Principales caractéristiques d’OCaml (1/3) Langage fonctionnel Le langage OCaml est : fonctionnel : les fonctions sont des valeurs de première classe, elles peuvent être retournées en résultat et passées en argument à d’autres fonctions. I I à gestion mémoire automatique (comme JAVA) fortement typé : le typage garantit l’absence d’erreur (de la machine) à l’exécution. I I compilé ou interactif (mode interactif = grosse calculette) Le langage est typé statiquement. Il possède un système d’inférence de types : le programmeur n’a besoin de donner aucune information de type : les types sont synthétisés automatiquement par le compilateur. Le polymorphisme paramétrique De plus le langage possède : I une couche à objets assez sophistiquée, I un système de modules très expressif. Programmation Fonctionnelle Une seule notion fondamentale : la notion de valeur. Les fonctions elles-mêmes sont considérées comme des valeurs. Un programme est essentiellement constitué d’expressions et le calcul consiste en l’évaluation de ces expressions. Le système de typage avec synthèse de types : les types sont facultatifs et synthétisés par le système. S. Guerrini (LIPN - Paris 13) Startups, CEA, EDF, France Telecom, Simulog,... Gros Logiciels Quelques succès : Coq, Ensemble, ASTREE, (voir la bosse d’OCaml) Plus récemment, outils système 1995 Compilateur vers du code natif + système de modules Introduction à la programmation fonctionnelle et à OCaml Calcul symbolique : Preuves mathématiques, compilation, interprétation, analyses de programmes. Prototypage rapide. Langage de script. Langages dédiés. Programmation distribuée (bytecode rapide). Enseignement et recherche Industrie I 1990 Implantation de Caml-Light par X. Leroy and D. Doligez Programmation Fonctionnelle I I 1981 Premières implantations de ML S. Guerrini (LIPN - Paris 13) Domaines de prédilection février 2013 7 / 131 Un type peut être non entièrement déterminé. Il est alors polymorphe. Ceci permet de développer un code générique utilisable pour di↵érentes structures de données tant que la représentation exacte de cette structure n’a pas besoin d’être connue dans le code en question. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 8 / 131 Introduction à la programmation fonctionnelle et à OCaml Introduction à OCaml Introduction à la programmation fonctionnelle et à OCaml Principales caractéristiques d’OCaml (2/3) Introduction à OCaml Principales caractéristiques d’OCaml (3/3) Mécanismes particuliers I I I I pattern matching : OCaml possède un mécanisme de programmation par cas’ très puissant. OCaml possède un mécanisme d’exceptions pour interrompre l’évaluation d’une expression. Ce mécanisme peut aussi être adopté comme style de programmation. Il y a des types récursifs mais pas de pointeurs. La gestion de la mémoire est di↵érente de celle du C. OCaml intègre un mécanisme de récupération automatique de la mémoire. Environnement de programmation I F F Autres aspects de OCaml I I I I I I OCaml possède des aspects impératifs. La programmation impérative est possible mais non souhaitée. OCaml interagit avec le C. OCaml possède des aspects objets. Les fonctions peuvent manipuler des objets. Les programmes peuvent être structurés en modules paramétrés (séparation du code et de l’interface). OCaml exécute des processus léger (threads). OCaml communique avec le réseau internet. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Introduction à la programmation fonctionnelle et à OCaml février 2013 9 / 131 OCaml dispose de trois modes d’exécution. F I le mode interactif : c’est une boucle d’interaction. La boucle d’interaction d’OCaml utilise du code-octet pour exécuter les phrases qui lui sont envoyées. compilation vers du code-octet (bytecode) interprété par une machine virtuelle (comme Java). compilation vers du code machine exécuté directement par un microprocesseur. OCaml dispose de nombreuses bibliothèques de programmes. S. Guerrini (LIPN - Paris 13) Modes d’utilisation de OCaml Programmation Fonctionnelle Introduction à la programmation fonctionnelle et à OCaml Compilation février 2013 10 / 131 Modes d’utilisation de OCaml Mode interactif Commandes I I I I I ocaml : lance la boucle d’interaction ocamlrun : interprète de code-octet ocamlc : compilateur en ligne de code-octet ocamlopt : compilateur en ligne de code natif ocamlmktop : constructeur de nouvelles boucles d’interaction Unités de compilation : c’est la plus petite décomposition d’un programme pouvant être compilée. I I Pour la boucle d’interaction, c’est une phrase du langage. On va voir plus loin ce qu’est une phrase. Pour les compilateurs en ligne, c’est un couple de fichiers (le source .ml et l’interface facultative .mli). Extensions des fichiers objets I I I L’option -c indique au compilateur de n’engendrer que le code objet. L’option -custom de ocamlrun permet de créer un code autonome il contient le code-octet du programme plus l’interprète du code-octet). S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle I I I I I I Les fichiers objets en code-octet ont pour extension .cmo, .cma (bibliothèque objet), .cmi (interface compilée). Les fichiers objets en code natif ont les extensions .cmx et .cmxa. Options du compilateur I L’option -I catalogue permet d’indiquer un chemin dans lequel se fera la recherche de fichiers sources ou compilés. La boucle possède plusieurs directives de compilation qui permettent de modifier de façon interactif son comportement. Ces directives commencent par # et se terminent par ;;. février 2013 11 / 131 I I I #quit;; sort de la boucle d’interaction #directory catalogue;; ajoute un chemin de recherche #cd catalogue;; change de catalogue courant #load "fichier objet";; charge un fichier .cmo #use "fichier source";; compile et charge un fichier .ml ou .mli #print depth profondeur;; modifie la profondeur d’affichage #trace fonction;; trace les arguments d’une fonction #untrace fonction;; enlève cette trace #untrace all;; enlève toute les traces On peut lancer la boucle d’interaction sous emacs par M-x run-caml. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 12 / 131 Introduction à la programmation fonctionnelle et à OCaml Modes d’utilisation de OCaml Premiers pas en OCaml Les phrases Les phrases Une phrase est une suite de caractères qui se termine par ;;. Il y a essentiellement quatre genres de phrases : Premiers pas en OCaml I I I I S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Premiers pas en OCaml février 2013 13 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Premiers pas en OCaml février 2013 14 / 131 Les expressions Les opérateur arithmétiques Une fois entré dans la boucle OCaml on peut évaluer des expressions. Sur les entier on a les opérateurs arithmétiques infixés shell $ ocaml --> appel de la boucle Objective Caml version 3.11.1 # 3;; --> expression à évaluer - : int = 3 --> réponse # #quit;; --> pour sortir de la boucle shell $ --> shell de système # 3 + 4 * 2;; - : int = 11 Mais attention, les opérateurs sont typés et ils ne sont pas “overloaded” # 3.0 + 2.5;; Characters 0-3: 3.0 + 2.5;; ^^^ Error: This expression has type float but an expression was expected of type int La réponse de l’évaluation d’une expression est sa valeur et son type. Les expressions plus simples sont les constants des types de base. 5.0;; : float = 5. true;; : bool = true "Hello, word!";; : string = "Hello, word!" S. Guerrini (LIPN - Paris 13) expressions. déclarations globales de valeurs. déclarations de types. déclarations d’exceptions. Les expressions Expressions constantes # # # - Les Les Les Les Les opérateurs arithmétiques sur les réels ont des symboles distingués. # 3.0 +. 2.0;; - : float = 5. Programmation Fonctionnelle février 2013 15 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 16 / 131 Premiers pas en OCaml Les expressions Premiers pas en OCaml Les opérateurs booléens et le if-then-else Conversion de types Pour utiliser un entier dans une expression qui contient un opérateur réel il faut utiliser une fonction de conversion explicite Les opérateurs booléens sont I I I # # # # # # - not : négation && ou & : et séquentiel || ou or : ou séquentiel # 3.0 +. 2;; Characters 7-8: 3.0 +. 2;; ^ Error: This expression has type int but an expression was expected of type float # 3.0 +. float 2;; - : float = 5. not false && false;; : bool = false not (false && false);; : bool = true true = false;; : bool = false 3 = 3;; : bool = true 4 + 5 >= 10;; : bool = false 2.0 *. 4.0 >= 7.0;; : bool = true Les opérateur de comparaison < > <= >= = sont les mêmes pour les entiers et les réels # 2.0 < 3.0;; - : bool = true I # if (3 < 4) then 1 else 0;; - : int = 1 # if (3 > 4) then 1 else 0;; - : int = 0 seulement une des branches then/else est évaluée. Programmation Fonctionnelle Premiers pas en OCaml # 2 < 3;; - : bool = true Mais attention, il faut faire des conversions de type si nécessaire # 2.0 < 3;; Characters 6-7: 2.0 < 3;; ^ Error: This expression has type int but an expression was expected of type float # 2.0 < float 3;; - : bool = true Expressions conditionnelles S. Guerrini (LIPN - Paris 13) Les expressions février 2013 17 / 131 S. Guerrini (LIPN - Paris 13) Les chaı̂nes de caractères Programmation Fonctionnelle Premiers pas en OCaml Les chaı̂nes de caractères février 2013 Les fonctions Les fonctions Literals Les fonctions sont des expressions (fun est équivalent à function) # "Hello, world!";; - : string = "Hello, world!" # # - Concaténation # "Hello, " ^ "world!";; - : string = "Hello, world!" fun x -> : int -> function : int -> x * 2;; int = <fun> x -> x *2;; int = <fun> La bibliothèque String Le type d’une fonction n’est plus un type de base Pour évaluer une fonction il faut lui donner les valeurs des arguments # String.uppercase "Hello, world!";; - : string = "HELLO, WORLD!" # (fun x -> x * 2) 3;; - : int = 6 # open String;; # uppercase "Hello, world!";; - : string = "HELLO, WORLD!" # (fun f -> f (f 2));; - : (int -> int) -> int = <fun> L’argument d’une fonction peut être une fonction Le résultat de l’évaluation d’une fonction peut être une autre fonction Pour accéder aux fonctions dans une bibliothèque : I I Biblio.f : pour utiliser la fonction f de la bibliothèque Biblio (noter que un nom de bibliothèque commence par une majuscule) open Biblio : pour accéder à toutes les fonctions de Biblio directement avec leur nom. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle 18 / 131 février 2013 19 / 131 # (fun f x -> f (f x)) (fun x -> x * x);; - : int -> int = <fun> En ajoutant des arguments on peut évaluer la fonction obtenue # (fun f x -> f (f x)) (fun x -> x * x) 2;; - : int = 16 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 20 / 131 Premiers pas en OCaml Les noms Premiers pas en OCaml Les noms Les noms Convention sur les noms On peut associer un nom à des expressions pour le réutiliser après # let x = 2 + 3;; val x : int = 5 # x * 3;; - : int = 15 En OCaml, les noms des variables et des fonctions doivent commencer par une minuscule. En particulier on peut donner un nom aux fonctions Les noms commençant par une majuscule sont réservés aux constructeurs. # let f = (fun x -> x * 2);; val f : int -> int = <fun> # f 3;; - : int = 6 # let IsEven x = x mod 2 = Characters 4-12: let IsEven x = x mod 2 = ^^^^^^^^ Error: Unbound constructor # let isEven x = x mod 2 = val isEven : int -> bool = Pour définir une fonction OCaml fournit aussi une syntaxe simplifié # let g f x = f (f (x * 2));; val g : (int -> int) -> int -> int = <fun> # let sq x = x * x;; val sq : int -> int = <fun> # let h = g sq;; val h : int -> int = <fun> # h 1;; - : int = 16 let f x y = e est équivalent à S. Guerrini (LIPN - Paris 13) 0;; IsEven 0;; <fun> Dans une déclaration, un nom x déclaré dans un let est connu dans toutes les expressions qui suivent la déclaration, mais pas dans l’expression à la droite de let x =. let f = fun x y -> e Programmation Fonctionnelle Premiers pas en OCaml 0;; février 2013 21 / 131 S. Guerrini (LIPN - Paris 13) Les noms Programmation Fonctionnelle Premiers pas en OCaml Porté du let février 2013 22 / 131 Les couples Les produits cartésiens Les valeurs des produits cartésiens binaires sont les couples # (3, 4);; - : int * int = (3, 4) Dans une déclaration, un nom x déclaré dans un let est connu dans toutes les expressions qui suivent la déclaration mais pas dans l’expression à la droite de let x = Ce n’est pas nécessaire que les objets de la couple aient le même type # let p = (true, "alpha");; val p : bool * string = (true, "alpha") Les projections # let z = z + 3;; Characters 8-9: let z = z + 3;; ^ Error: Unbound value z # let z = 2;; val z : int = 2 # let z = z + 3;; val z : int = 5 # # # - fst p;; : bool = snd p;; : string p = (fst : bool = true = "alpha" p, snd p);; true Les produits cartésiens n-aires Pour définir des fonctions récursives il faut noter qu’on est en train de donner une déclaration récursive en utilisant le construit let rec. # (true, 3, "alpha", 4);; - : bool * int * string * int = (true, 3, "alpha", 4) I I I les projections fst et snd sont prédéfinies seulement pour le cas binaire n = 2 il n’y a pas des projections prédéfinies pour n 6= 2 mais, on peut définir les projections pour toutes les n-uplets # let p_1_3 (x, y, z) = x;; val p_1_3 : ’a * ’b * ’c -> ’a = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 23 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 24 / 131 Premiers pas en OCaml Les couples Les fonctions Fonctions n-aires Fonction n-aire ou sur n-uplets Une fonction binaire # let f x y = x + y;; val f : int -> int -> int = <fun> en e↵et, c’est une fonction unaire qui renvoie une fonction unaire On peut définir une fonction binaire en utilisant un couple Les fonctions # let g (x,y) = x + y;; val g : int * int -> int = <fun> en e↵et, cette fonction est une fonction unaire sur une couple La définition précédente est équivalente à # let g p = fst p + snd p;; val g : int * int -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les fonctions février 2013 25 / 131 S. Guerrini (LIPN - Paris 13) Fonctions n-aires Programmation Fonctionnelle Les fonctions Exemples février 2013 26 / 131 La récursion Fonctions récursives On peut toujours transformer une fonction binaire en une fonction qui prend ses deux arguments comme éléments d’une couple : # let curry f (x, y) = f x y;; val curry : (’a -> ’b -> ’c) -> ’a * ’b -> ’c = <fun> # let f1 x y = x + y;; val f1 : int -> int -> int = <fun> # f1 3 4;; - : int = 7 Une définition de fonction peut être récursive, mais il faut ajouter le mot clé rec à let # let rec exp x = if (x = 0) then 1 else if (x mod 2 = 1) then (exp (x/2)) * (exp (x/2)) * 2 else (exp (x/2)) * (exp (x/2));; val exp : int -> int = <fun> # let f2 = curry f1;; val f2 : int * int -> int = <fun> # f2 (3,4);; - : int = 7 La définition précédente est très inefficace car elle évalue deux fois la même expression récursive dans chaque branche du cas x > 0. Pour éviter ce type de problèmes on peut définir des noms locaux # f3 3 4;; - : int = 7 # let rec exp x = if (x = 0) then 1 else let h = exp (x/2) in if (x mod 2 = 1) then h * h * 2 else h * h;; On peux aussi bien définir la fonction inverse de curry # let uncurry f x y = f (x,y);; val uncurry : (’a * ’b -> ’c) -> ’a -> ’b -> ’c = <fun> # let f3 = uncurry f2;; val f3 : int -> int -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 27 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 28 / 131 Les fonctions La récursion Les fonctions Fibonacci Fonctions itératives La fonction de Fibonacci # let rec fib n = if n < 2 then 1 else (fib (n-1) + fib (n-2));; val fib : int -> int = <fun> La précédente fonction n’est pas trop efficace : le problème est qu’il y a une double appel de la fonction récursive. Essayez de calculer fib 40;; On peut pourtant coder de façon “récursive” la version “itérative” de Fibonacci qui calcul au meme temps fib n;; et fib (n-1);; On n’a pas encore de commandes pour l’itération (while ou repeat). Mais la possibilité de définir des fonctions d’ordre supérieure (fonctions qui prennent comme arguments des fonctions et revoient des fonctions comme résultat) nous permet de définir une fonctionnelle iter qui prend 1 2 3 4 let fibonacci n = let rec fib n = if n = 0 then (1,0) else let p = fib (n-1) in let x = fst p and y = snd p in (x+y, x) in fst (fib n);; let rec fib n = if n = 0 then (1,0) else let p = fib (n-1) in let x = fst p and y = snd p in (x+y, x);; un entier n, une valeur f0 de type ↵ (par exemple : un entier), une fonction f1 : ↵ ! ↵ (par exemple : une fonction sur les entiers qui double son argument) et calcul f1n (f0 ) (qu’est-ce qu’on obtient avec les exemples précédentes ?) let rec iter n f0 f1 = if n = 0 then f0 else f1 (iter (n-1) f0 f1);; Programmation Fonctionnelle Les fonctions février 2013 let dup x = 2 * x;; let exp2 n = fst (iter n 1 dup);; On peut aussi bien utiliser iter pour définir fibonacci : let fibstep (x,y) = (x+y, x);; S. Guerrini (LIPN - Paris 13) La récursion 29 / 131 S. Guerrini (LIPN - Paris 13) Les déclarations Programmation Fonctionnelle Les fonctions Déclarations globales et déclarations locales let fibonacci n = fst (iter n (1,0) fibstep);; février 2013 30 / 131 Les déclarations Déclarations et expressions Une déclaration globale associe un nom à une valeur # let x = 3 * 4;; val x : int = 12 En réponse à une déclaration globale, la boucle affiche l’e↵et que la déclaration a eu sur l’environnement globale (en ajoute au type et à la valeur de l’expression). Dans l’exemple, val x indique que 1 2 le nom x a été ajouté à l’environnement ; que le nom x est associé à une valeur. # 4 * let x = 2 in x + 4;; - : int = 24 Une déclaration globale n’est pas une expression # 4 * let x = 2;; Characters 14-16: 4 * let x = 2;; ^^ Error: Syntax error Dans une déclaration locale let x = 3 in x + 2;; le nom déclaré n’est connu que dans l’expression qui suive le in. Une déclaration locale peut redéfinir localement un nom global # let x val x : # let x - : int # x;; - : int Une déclaration locale est une expression et on peut l’utiliser comme une autre expression quelconque du même type Une déclaration globale peut être utiliser seulement dans des contextes particuliers, par exemple, comme commande pour la boucle. = 2;; int = 2 = 3 in x;; = 3 = 2 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 31 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 32 / 131 Les fonctions Les déclarations Les fonctions Noms OCaml vs. variables C Déclarations simultanées et recursion mutuelle Déclarations simultanées Dans une déclaration globale ou locale on peut définir plusieurs variable au même temps et en parallèle dans une déclaration simultanée. Les noms OCaml et les variables C sont des objets de natures complètement di↵érentes. I I I I En OCaml, une variable est un nom pour une valeur, alors qu’en C une variable est un nom pour une case mémoire. En OCaml, il est donc hors de question de pouvoir changer la valeur d’une variable. La déclaration globale (ou locale, voir plus loin) d’OCaml, n’a rien à voir avec l’a↵ectation du C. Pour obtenir des variables OCaml avec un comportement similaire aux variables C il faut utiliser F F des données d’un type particulier, les enregistrements à champs modifiables, et les références (ou pointers). # let x val x : val y : # let x - : int = 3 and y = 4;; int = 3 int = 4 = y and y = x in (x,y);; * int = (4, 3) Dans une déclaration simultanée les expressions sont évaluées avant d’associer les noms de la déclaration aux valeurs des expressions correspondantes. Une déclaration simultanée n’est pas équivalent à une chaı̂ne de déclaration # # - let x : int let y : int = * = * y in let y = x in (x,y);; int = (4, 4) x in let x = y in (x,y);; int = (3, 3) Dans une chaı̂ne de déclarations l’ordre est relevant, tandis que dans une déclaration simultanée l’ordre des déclarations n’a aucun influence sur le résultat # let y = x and x = y in (x,y);; - : int * int = (4, 3) S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les fonctions février 2013 33 / 131 S. Guerrini (LIPN - Paris 13) Déclarations simultanées et recursion mutuelle Programmation Fonctionnelle Les fonctions février 2013 34 / 131 février 2013 36 / 131 Déclarations simultanées et recursion mutuelle La récursion mutuelle Les déclarations récursives simultanées permettent de définir des fonctions mutuellement récursives Listes polymorphes # let rec isOdd x = if (x = 0) then false else isEven (x-1) and isEven x = if (x = 0) then true else isOdd (x-1);; val isOdd : int -> bool = <fun> val isEven : int -> bool = <fun> # let and g val val g rec f x = if x <= 1 then 1 else g (x+2) x = f (x-3) + 4;; f : int -> int = <fun> : int -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 35 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml Les type list Les listes en OCaml Listes polymorphes Les type list Construction de listes (1/2) La liste vide [] est une valeur de type polymorphe Objective Caml permet de générer des listes d’objets de même type sans que ce type soit précisé. On parle pourtant de listes polymorphes. A partir d’un type quelconque ↵ on peut construire les listes d’éléments de type ↵. # [];; - : ’a list = [] L’opérateur infixé :: ou cons ajoute en élément en tête à une liste # # - 1 :: [];; : int list = [1] true :: [];; : bool list = [true] On rappel qu’on n’a pas le droit de mélanger des éléments de types di↵érents dans une liste. Les listes d’éléments de type ↵ ont type : ’a list # 1 :: [true];; Characters 6-10: 1 :: [true];; ^^^^ Error: This expression has type bool but an expression was expected of type int On peut construire des listes d’un type quelconque, mais dans une liste tous les éléments ont le même type. Le type ’a list est en fait un schéma de type prédéfini. La plupart des fonctions de manipulation des listes sont définies dans la bibliothèque List. On note [e1;...;en] la liste e1 :: (e2 :: ... (en :: []) ... ) # 1 :: 2 :: -2 :: 0 :: 4 :: [];; - : int list = [1; 2; -2; 0; 4] S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml février 2013 37 / 131 Les type list 38 / 131 Les type list La déstructuration des listes (l’extraction des éléments de la liste) se fait par pattern matching. # [1;2] @ [3;4;5];; - : int list = [1; 2; 3; 4; 5] # let f = function [] -> 0 | x :: l -> x+1;; val f : int list -> int = <fun> Attention à la di↵érence entre et # [1;2] :: [];; - : int list list = [[1; 2]] Les éléments d’une listes peuvent être des listes. A la place de donner le nom de l’argument de la fonction, on donne des motifs (patterns) qui correspondent à des valeurs possibles pour l’argument de la fonction. # [4;5] :: [[1;2];[];[6];[7;8;5;2]];; - : int list list = [[4; 5]; [1; 2]; []; [6]; [7; 8; 5; 2]] Les listes de listes polymorphes ont type : ’a list list A chaque motif m est associée une expression e # [[]];; - : ’a list list = [[]] m -> e Si le motif m correspond à la valeur de la variable de la fonction alors le résultat est la valeur de l’expression e Par exemple [] -> 0 indique que si l’argument de f est la liste vide, alors le résultat est 0. et aussi bien [[[]]];; : ’a list list list = [[[]]] [[[2;3]; [1]];[[]]; [[3]]];; [[[2;3]; [1]];[[]]; [[3]]];; : int list list list = [[[2; 3]; [1]]; [[]]; [[3]]] S. Guerrini (LIPN - Paris 13) février 2013 Déstructuration des listes (1/3) L’opérateur infixé @ (opérateur de concaténation) concatène deux listes # # - Programmation Fonctionnelle Les listes en OCaml Construction de listes (2/2) # [1;2] @ [];; - : int list = [1; 2] S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle # f [];; - : int = 0 février 2013 39 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 40 / 131 Les listes en OCaml Les type list Les listes en OCaml Déstructuration des listes (2/3) Déstructuration des listes (3/3) Les pairs motif->expression (p -> e) sont séparées par une barre verticale p1 -> e1 | p2 -> e2. Un motif peut contenir des variables qui, si le motif correspond à la valeur de l’argument de la fonction, seront associés à une partie de la valeur de l’argument. Par exemple, le motif de x :: l -> x+1 I Pour prendre les éléments d’une liste on peut également utiliser le fonctions hd (head) e tl (tail) de la librairie List. # List.hd [1;3;5];; - : int = 1 # List.tl [1;3;5];; - : int list = [3; 5] Pourtant correspond à une liste non vide. I Les type list La valeur de tête de la liste est associée à la variable x. La queue de la liste (c’est-à-dire ce qui reste de la liste sans la tête) est associée à la variable l. # (function l -> List.hd l :: List.tl l) [1;3;5];; - : int list = [1; 3; 5] La fonction f qu’on viens de voir on pourrait l’écrire Les valeurs associés aux variables dans un motif sont utilisées pour le calcul de l’expression associée au motif. Pourtant, le motif précédent dit que le résultat de la fonction sur une liste non vide est la valeur de la tête plus 1. # let f l = if (l == []) then 0 else List.hd l + 1;; val f : int list -> int = <fun> # f [3;2;7];; - : int = 4 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml février 2013 41 / 131 S. Guerrini (LIPN - Paris 13) Les type list Programmation Fonctionnelle Les listes en OCaml Motifs février 2013 42 / 131 février 2013 44 / 131 Fonctions récursive sur les listes Fonctions récursives sur les listes Les motifs peuvent être beaucoup plus compliqués des cas qu’on a vu. Par exemple, I I pour définir une fonction qui prend une liste de listes d’entiers et ajoute 1 au premier éléments de la tête de l’argument (et si cet élément n’existe pas alors la fonction renvoie une constante) il faut que le motif analyse la tête d’une liste non vide. # let f = [] -> | [] :: | (x :: val # # - f : f : function 0 l -> 1 l1) :: l2 -> x+1;; f : int list list -> int = <fun> La somme des éléments d’une liste d’entiers. # let rec somme = function [] -> 0 | x :: l -> x + somme l;; val somme : int list -> int = <fun> # somme [3;2;7];; - : int = 12 La conjonction des éléments d’une liste de booléens. # let rec conj = function [] -> true | x :: l -> x & conj l;; val conj : bool list -> bool = <fun> [ []; [1;2] ];; int = 1 [ [3;2]; []; [1;4] ];; int = 4 On analysera en détail le pattern matching plus avant. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 43 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml Fonctions récursive sur les listes Les listes en OCaml La longueur d’une liste Fonctionnelle récursives sur les listes La fonctionnelle map (1/2) La longueur d’une liste est l’exemple classique de fonction polymorphe sur les listes : pour la calculer on n’a pas besoin de connaitre le type des éléments de la liste. # let rec longueur = function [] -> 0 | _ :: l -> longueur l + 1;; val longueur : ’a list -> int = <fun> longueur [true; false];; : int = 2 longueur [[2]];; : int = 1 En utilisant la fonction List.tl, on peut écrire # let rec longueur l = if l == [] then 0 else 1 + longueur (List.tl l);; val longueur : ’a list -> int = <fun> Considérons l’exemple suivant, qui ajoute 1 à tout élément d’une liste. # let rec succl = function [] -> [] | x :: l -> (x+1) :: succl l;; val succl : int list -> int list = <fun> # succl [2;1;5];; - : int list = [3; 2; 6] # # - Voici maintenant une fonction qui, à une liste d’entiers [n1; . . . ; nk], associe la liste de booléens [b1; . . . ;bk] telle que bi soit true si et seulement si ni est pair. aussi : On peut enfin utiliser la fonction length de la bibliothèque List. Attention, même la fonction de la bibliothèque prend un temps proportionnel à la longueur de la liste. # let rec pairl = function [] -> [] | x :: l -> ((x mod 2) = 0) :: pairl l;; val pairl : int list -> bool list = <fun> # pairl [1;4;6;3;8];; - : bool list = [false; true; true; false; true] # List.length [1;4;5];; - : int = 3 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml février 2013 45 / 131 Fonctionnelle récursives sur les listes Programmation Fonctionnelle Les listes en OCaml La fonctionnelle map (2/2) février 2013 Fonctionnelle récursives sur les listes La librairie List définie aussi deux fonctionnelles d’itération # List.fold_right;; - : (’a -> ’b -> ’b) -> ’a list -> ’b -> ’b = <fun> # List.map;; - : (’a -> ’b) -> ’a list -> ’b list = <fun> # List.fold_left;; - : (’a -> ’b -> ’a) -> ’a -> ’b list -> ’a = <fun> telle que List.map f[e1;...;en] = [f e1;...;f en] Ces deux fonctionnelles permettent l’écriture compacte d’une fonction de calcul sur les éléments d’une liste comme la somme des éléments. peut s’écrire très facilement # let rec map f = function [] -> [] | x :: l -> f x :: map f l;; val map : (’a -> ’b) -> ’a list -> ’b list = <fun> Les e↵ets de fold right et fold left sont les suivants. fold right f [e1;e2;...;en] a = (f e1 (f e2 ... (f en a)...)) fold left f a [e1;e2;...;en] = (f ... (f (f a e1) e2) ... en) On peut alors réécrire les expressions vues plus haut en La fold right correspond à un schéma général de récursion sur les listes. # List.map (function x->x+1) [3; 2; 6];; - : int list = [4; 3; 7] En fait, let g l = fold rigth f l a definie une fonction telle que g [] = a g (x :: l) = f x (g l) # List.map (function x->(x mod 2)=0) [1;4;6;3;8];; - : bool list = [false; true; true; false; true] S. Guerrini (LIPN - Paris 13) 46 / 131 Fonctionnelles d’itérations sur les listes (1/4) La librairie List définie la fonctionnelle map S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle La fold left correspond plutôt à une iteration sur les élements de la liste de l’externe vers l’interne. février 2013 47 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 48 / 131 Les listes en OCaml Fonctionnelle récursives sur les listes Les listes en OCaml Fonctionnelles d’itérations sur les listes (2/4) Fonctionnelle récursives sur les listes Fonctionnelles d’itérations sur les listes (3/4) et fold left permettent d’écrire très rapidement et dans une forme concise les fonction récursives sur les listes. fold right Par exemple, la fonction qui somme les éléments d’une liste peut s’écrire On peut également redéfinir la fonctionnelle map. # let somme l = List.fold_right (function x -> function y -> x+y) l 0;; val somme : int list -> int = <fun> # let map f ls = List.fold_right (fun x l -> (f x) :: l) ls [];; val map : (’a -> ’b) -> ’a list -> ’b list = <fun> De même, la fonction qui concatène deux listes peut s’écrire : # somme [3;1;9];; - : int = 13 # let append l1 l2 = List.fold_right (fun x ls -> x :: ls) l1 l2;; val append : ’a list -> ’a list -> ’a list = <fun> ou sinon # let somme l = let add x y = x+y in List.fold_right add l 0;; val somme : int list -> int = <fun> # append [1;2;3] [4;5;6;7];; - : int list = [1; 2; 3; 4; 5; 6; 7] L ’écriture directe de fold right peut se faire de la façon suivante. # List.append [1;2;3] [4;5;6;7];; - : int list = [1; 2; 3; 4; 5; 6; 7] La fonction append est prédéfinie dans la bibliothèque List. # let rec fold_right f ls a = match ls with [] -> a | x :: l -> f x (fold_right f l a);; val fold_right : (’a -> ’b -> ’b) -> ’a list -> ’b -> ’b = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml février 2013 49 / 131 S. Guerrini (LIPN - Paris 13) Fonctionnelle récursives sur les listes Programmation Fonctionnelle Les listes en OCaml Fonctionnelles d’itérations sur les listes (4/4) février 2013 50 / 131 février 2013 52 / 131 Fonctionnelle récursives sur les listes Quelques autres fonctions du module List L ’écriture directe de fold left peut se faire de la façon suivante. # let rec fold_left f a = function [] -> a | x :: l -> fold_left f (f a x) l;; val fold_left : (’a -> ’b -> ’a) -> ’a -> ’b list -> ’a = <fun> # # # # # # - En utilisant fold left on peut définir la function qui inverse une liste. # let rev = List.fold_left (fun l x -> x :: l) [];; val rev : ’_a list -> ’_a list = <fun> # rev [1;2;3;4];; - : int list = [4; 3; 2; 1] La fonction List.mem;; : ’a -> ’a list -> bool = <fun> List.flatten;; : ’a list list -> ’a list = <fun> List.find;; : (’a -> bool) -> ’a list -> ’a = <fun> List.split;; : (’a * ’b) list -> ’a list * ’b list = <fun> List.combine;; : ’a list -> ’b list -> (’a * ’b) list = <fun> List.assoc;; : ’a -> (’a * ’b) list -> ’b = <fun> # List.rev;; - : ’a list -> ’a list = <fun> est prédéfinie dans la librairie List. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 51 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les listes en OCaml Fonctionnelle récursives sur les listes Synthèse de types et polymorphisme Synthèse de types Synthèse de types OCaml est un langage fortement typé I I I tous les expressions ont un type le système vérifie que la construction des expressions respect le typage il n’y a pas des conversions implicites de type Dans une expression ou dans la déclaration d’une variable il n’y a pas des indications de type (mais on verra qu’on peut l’ajouter). Synthèse de types En OCaml les types sont synthétisés. L’algorithme de synthèse des types prend une expression et I I soit trouve un type pour l’expression soit trouve qu’il y a un erreur de typage. # fun x -> x + (x & true);; Characters 15-16: fun x -> x + (x & true);; ^ Error: This expression has type int but an expression was expected of type bool S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Synthèse de types et polymorphisme février 2013 53 / 131 S. Guerrini (LIPN - Paris 13) Synthèse de types Synthèse de types et polymorphisme Synthèse du type d’une fonction 54 / 131 Synthèse de types Dans certains cas, une expression est bien typé mais I I Dans l’expression précédente : la variable y est utilisée dans deux expressions entiers (y*y et y+1) et donc elle est de type int ; la variable x est utilisée comme conditionnel d’un if et donc elle est de type bool ; avec ces hypothèses le if-then-else est bien typé parce que l’expression conditionnel est de type bool et les branches then et else ont le même type ; le type du valeur renvoyé par le if-then-else (et donc de tout l’expression if-then-else) est le type des branches then/else et donc c’est int ; le type de tout l’expression fun est bool -> int -> int c’est-à-dire le type d’une fonction qui prend un argument de type bool (la variable x), un argument de type int (la variable y) et renvoie un résultat de type int (le résultat de le if-then-else). Programmation Fonctionnelle février 2013 Le polymorphisme # fun x y -> if x then y+1 else y*y;; - : bool -> int -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 55 / 131 le type de certains paramètres ou de la valeur de retour ne sont pas complètement précisés ; ces types ne sont pas nécessaire pour déterminer la valeur de l’expression à partir de ces paramètres. # fun x -> x;; - : ’a -> ’a = <fun> # let eq x y = x = y;; val eq : ’a -> ’a -> bool = <fun> (qu’on lit ↵) est une variable de type et indique que la fonction c’est bien définie pour un type quelconque ↵. Les fonctions dont l’un (ou plusieurs) des paramètres ou la valeur de retour est d’un type qu’il n’est pas nécessaire de préciser sont dites fonctions polymorphes. Le type d’une fonction polymorphe peut contenir plus d’une variable de type ’a, ’b, ’c, . . . (qu’on lit ↵, , , . . . ) ’a # let first val first : # let third val third : = function (x,y) -> x;; ’a * ’b -> ’a = <fun> (x, y, z) = z;; ’a * ’b * ’c -> ’c = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 56 / 131 Synthèse de types et polymorphisme Synthèse de types Synthèse de types et polymorphisme Utilisation de fonctions polymorphes Synthèse de types Des autres exemples de fonctions polymorphes Une fonction polymorphe peut être utilisé dans tour les contextes compatible avec le type de la fonction # # - first (2,3);; : int = 2 first ("toto",true);; : string = "toto" # # - eq 2 3;; : bool = false eq true true;; : bool = true Si le contexte n’est pas compatible, on a un erreur de typage # eq true 3;; Characters 8-9: eq true 3;; ^ Error: This expression has type int but an expression was expected of type bool Remarque Ce type d’erreur est trouvé par le compilateur avant d’évaluer la fonction. I Le typage sert à garantir qu’on n’a pas des erreurs à temps d’exécution liés à une fonction qui reçoit une valeur de type incorrect. I Le polymorphisme de OCaml assure une très grande flexibilité et praticité dans la définition des fonctions, sans perdre les avantage du typage. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Synthèse de types et polymorphisme février 2013 57 / 131 # let compose f g x = f (g x);; val compose : (’a -> ’b) -> (’c -> ’a) -> ’c -> ’b = <fun> S. Guerrini (LIPN - Paris 13) février 2013 58 / 131 Synthèse de types Synthèse de types polymorphes Si une occurrence d’une variable f a type ⌧1 e une autre a type ⌧2 , le système cherche une type ⌧ qui est un cas particulier de ⌧1 et de ⌧2 . # fun f g x -> (f (g x), g (f x));; - : (’a -> ’a) -> (’a -> ’a) -> ’a -> ’a * ’a = <fun> la première occurrence de id a le type bool -> bool la seconde a le type int -> int Cependant 1 # let g f = ((f true),(f 2));; Characters 23-24: let g f = ((f true),(f 2));; ^ Error: This expression has type int but an expression was expected of type bool 2 Les contraints qui OCaml pose au polymorphisme empêchent que le type de la fonction f soit polymorphe. La synthèse des types de OCaml ne permet pas de partir de deux type particuliers et de trouver un type plus générale : 2 Programmation Fonctionnelle Synthèse de types et polymorphisme # let id x = x;; val id : ’a -> ’a = <fun> # ((id true),(id 2));; - : bool * int = (true, 2) 1 = 3 # let g = function f -> f 0;; val g : (int -> ’a) -> ’a = <fun> # g (function x->x+1);; - : int = 1 # g (function x->if x=0 then "toto" else "tata");; - : string = "toto" Synthèse de types Les limites du polymorphisme en OCaml 2 x = (x, x);; ’a -> ’a * ’a = <fun> x = first (d x);; ’a -> ’a = <fun> La fonction compose qui compose deux fonctions : I 1 # let d val d : # let g val g : # g 3;; - : int (f true) force que f soit de type bool -> ’a (f 2) force que f soit de type int -> ’b dans f (g x) on a : dans g (f x) on a : x: ’a x: ’d g: ’a -> ’b f: ’d -> ’e f: ’b -> ’c g: ’e -> ’f Pour le typage de l’expression complète, il faut trouver des valeurs pour les variables de type tels que, pour le typage 1 2 3 de x : de g : de f : ’a = ’d ’a -> ’b = ’e -> ’f ’b -> ’c = ’d -> ’e implique implique ’a = ’e et ’f = ’b ’b = ’d et ’c = ’e Par conséquent, la solution est ’a = ’d = ’b = ’f = ’e = ’c Qui force x: ’a f: ’a -> ’a g: ’a -> ’a on ne peut pas conclure que : “f:’a -> ’a”. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 59 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 60 / 131 Synthèse de types et polymorphisme Synthèse de types Synthèse de types et polymorphisme Unification Le type le plus général L’algorithme de synthèse des types est un algorithme d’unification de termes. Si dans une expression, I I I I l’utilisation d’un identificateur x dans une contexte force le type ⌧1 pour x, tandis que l’utilisation de x dans une autre contexte force le type ⌧2 , il faut unifier ⌧1 et ⌧2 , alors il faut trouver une substitution pour les variables de type de ⌧1 et ⌧2 (une fonction ✓ qui replace toutes ou une partie des variables de ⌧1 avec des autres expressions de type) tel que on obtient le même type ⌧ en appliquant la substitution à ⌧1 et ⌧2 (c’est-à-dire ✓(⌧1 ) = ⌧ = ✓(⌧2 )). Exemple : I I I I ⌧1 = (↵ ! ) ! ⇥ ↵ ⌧2 = ! ✓ = { 7! ⇥ ↵, 7! ↵ ! ⇥ ↵} ⌧ = ✓(⌧1 ) = ✓(⌧2 ) = (↵ ! ⇥ ↵) ! S. Guerrini (LIPN - Paris 13) Synthèse de types Synthèse de types et polymorphisme I I 2 février 2013 61 / 131 s’il y a une autre type ⌧ 0 compatible avec l’utilisation de l’identificateur, alors ⌧ 0 est un cas particulier de ⌧ , c’est-à-dire, il y a une substitution tel que ⌧ 0 = (⌧ ). Pour déterminer le type le plus général l’algorithme d’unification de termes trouve toujours l’unificateur le plus général ou mgu (most general unifier), s’il existe. Une substitution ✓ qui unifie ⌧1 et ⌧2 (et donc ⌧ = ✓(⌧1 ) = ✓(⌧2 )) est le mgu de ⌧1 et ⌧2 si 1 ⇥↵ Programmation Fonctionnelle En utilisant l’unification, le compilateur calcule le type “le plus général”l qu’on puisse donner aux identificateurs et à l’expression elle-même qui soit compatible avec les di↵érentes constructions syntaxiques utilisées. Le type le plus général ⌧ d’un identificateur a la propriété que pour chaque substitution ✓0 qui unifie ⌧1 et ⌧2 (et donc ⌧ 0 = ✓0 (⌧1 ) = ✓0 (⌧2 )) il existe une substitution tel que ✓ = ✓0 (et donc (⌧ ) = ⌧ 0 ). S. Guerrini (LIPN - Paris 13) Synthèse de types Programmation Fonctionnelle Synthèse de types et polymorphisme Contraintes de type (1/4) février 2013 62 / 131 Synthèse de types Contraintes de type (2/4) Comme le synthétiseur de types en OCaml engendre le type le plus général, il peut être utile ou nécessaire de préciser le type t d’une expression expr La contrainte de type peut être adjointe à n’importe quelle expression ou sous-expression, y compris les arguments d’une fonction. # let idint (x : int) = x;; val idint : int -> int = <fun> ( expr : t ) # let idint x = (x : int);; val idint : int -> ccint = <fun> Lorsqu’il rencontre une telle contrainte, le synthétiseur de type en tiendra compte pour la construction du type de l’expression. Cette restriction du polymorphisme permet de mieux contrôler le type des expressions en restreignant le polymorphisme du type déduit par le système. # let idint = (function (x : int) -> x);; val idint : int -> int = <fun> # let idint = (function x -> (x : int));; val idint : int -> int = <fun> Il s’agit ici de contraintes et non d’un typage explicite remplaçant la synthèse de type d’OCaml. # let idint = (function x -> x : int -> int);; val idint : int -> int = <fun> Dans une contrainte, on peut utiliser n’importe quel type défini, pouvant contenir des variables de type. # let idint (x : (’a -> int) -> ’b) = x;; val idint : ((’a -> int) -> ’b) -> (’a -> int) -> ’b = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 63 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 64 / 131 Synthèse de types et polymorphisme Synthèse de types Synthèse de types et polymorphisme Contraintes de type (3/4) Contraintes de type (4/4) L’usage des contraintes de type permet : de rendre visible le type des paramètres d’une fonction ; de spécifier le type d’une expression. # let nil = ([] : int list);; val nil : int list = [] # let mult (x : int) (y : int) = x * y;; val mult : int -> int -> int = <fun> d’interdire l’utilisation d’une fonction en dehors de son cadre ; # (fun (n : int) ls -> n :: ls) ’a’;; Characters 30-33: (fun (n : int) ls -> n :: ls) ’a’;; ^^^ Error: This expression has type char but an expression was expected of type int # let cons (n : int) ls = n :: ls;; val cons : int -> int list -> int list = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Synthèse de types et polymorphisme Synthèse de types février 2013 65 / 131 # 3 :: nil;; - : int list = [3] # ’a’ :: nil;; Characters 7-10: ’a’ :: nil;; ^^^ Error: This expression has type int list but an expression was expected of type char list S. Guerrini (LIPN - Paris 13) Synthèse de types Programmation Fonctionnelle Synthèse de types et polymorphisme février 2013 66 / 131 février 2013 68 / 131 Synthèse de types Définitions de types En OCaml on peut associer un nome à un type # type fint = int -> int;; type fint = int -> int Gestion de la mémoire On peut bien définir des types paramétrique ou polymorphes # type ’a ftype = ’a -> ’a;; type ’a ftype = ’a -> ’a # type (’a, ’b) pair = ’a * ’b;; type (’a, ’b) pair = ’a * ’b S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 67 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Gestion de la mémoire Mémoire de programme Gestion de la mémoire Des zones di↵érents de mémoire Mémoire de programme Garbage collection Mémoire statique I I I allouée au moment du chargement du programme (e.g., au démarrage de la boucle interactive). ne change pas de dimension, mais sont contenu peut se modifier au long de l’exécution. contient des données (ou du code) nécessaires tout au long de l’exécution du programme (e.g., le code et les données de la boucle interactive). Mémoire dynamique : le tas (en anglais, heap) I Des structures (e.g., des listes) sont ajoutées ou supprimées dynamiquement. Pile d’exécution (en anglais : stack) I I I I Comme dans le tas, des données sont ajoutées et supprimées dynamiquement. Mais, dans la pile, les données sont supprimées en respectant l’ordre inverse d’allocation (DEPS) La pile est organisée en cadres de piles (en anglais, stack frames) qui contiennent les données des appels de fonctions. La pile contient aussi des valeurs intermédiaire nécessaires seulement pendant l’évaluation di une expression de la fonction. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Gestion de la mémoire février 2013 69 / 131 I C’est la tâche du Garbage Collector (GC, parfois Ramassage de miettes), qui est lancé quand le système OCaml a besoin de mémoire. Comme en Java. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 70 / 131 Mémoire de programme Appel terminal Une fonction f appèle une fonction g et le résultat de l’évaluation de g est envoyé tout de suite par f. les valeurs locales et les paramètres d’un appel de fonction les informations nécessaires pour revenir au point de l’appel de la fonction I Quand cet appel de fonction est terminé, sa mémoire locale est balayée du sommet de la pile. Dans l’évaluation d’une fonction récursive on a un cadre de pile pour chaque appel récursif. I Quand une structure (par exemple une liste) n’est plus accessible alors son espace mémoire peut être réutilisé. Gestion de la mémoire A chaque appel de fonction, OCaml alloue un cadre sur la pile. Chaque cadre de pile stockes (parmi d’autres) I Le structures sont alloué au moyen des constructeur du type de données (e.g., pour les liste le cons ::). Mémoire de programme La pile I En OCaml on n’a pas des commandes explicites d’allocation ou de-allocation de la mémoire. Pourtant, si on fait une récursion trop profonde on risque d’épuiser l’espace disponible pour la pile. Mais attention, le risque est réel seulement si ont fait au moins des dizaines ou centaines de milliers d’appels récursives. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 71 / 131 exemple d’appel terminal : let f x = if x = 0 then 0 else g x I exemple d’appel non-terminal : let f x = x + g x Dans le cas d’appel terminal, on n’a plus besoin des valeurs locales de la fonction f quand on évalue g. Le cadre de f sur la pile peut être libéré avant d’évaluer g. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 72 / 131 Gestion de la mémoire Mémoire de programme Gestion de la mémoire Récursion terminale Mémoire de programme Réécrire une fonction en récursion terminale Il est parfois possible de réécrire une fonction récursive en une fonction récursive terminale équivalente. En anglais, tail recursion. let rec somme = function | [] -> 0 | hd :: tl -> hd + somme tl;; C’est une fonction récursive, avec tous les appels récursifs à des positions terminales. let somme ls = let rec somme_term acc = function | [] -> acc | hd :: tl -> somme_term (hd + acc) tl in somme_term 0 ls;; Normalement, il faut ajouter des paramètres à la fonction. let rec fold_left f b = function | [] -> b | h::r -> fold_left f (f b h) r;; I I Peut importe la profondeur de récurrence, l’utilisation d’espace reste constante : Une fonction récursive terminale correspond à une version itérative de la fonction. Les paramètres additionnels sont les données (l’état) modifiées à chaque itération. let fibonacci n = let rec fib n = if (n = 0) then (1,0) else let (a,b) = fib (n-1) in (a+b, a) in fst (fib n);; La fonction récursive est exécutée comme une boucle d’itération ! let fibonacci n = let rec fib (a,b) n = if (n = 0) then (a,b) else fib (a+b, a) (n-1) in fst (fib (1, 0) n);; La récursion terminale permet d’éviter des dépassements de pile (en anglais, stack overflow). S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Gestion de la mémoire février 2013 73 / 131 S. Guerrini (LIPN - Paris 13) Mémoire de programme Programmation Fonctionnelle Gestion de la mémoire février 2013 74 / 131 Mémoire de programme Récursion terminale et fonctions fold La fonction fold left est récursive terminale : let rec fold_left f b = function | [] -> b | h::r -> fold_left f (f b h) r;; Types enumerés et récurifs La fonction fold right n’est pas récursive terminale : let rec fold_right f l b = match l with | [] -> b | h::r -> f h (fold_right f r b);; S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 75 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 76 / 131 Gestion de la mémoire Mémoire de programme Les types énumérés Somme de types Les types énumérés La définition d’un type se fait à l’aide du mot clé type. Un constructeur permet de construire les valeurs d’un type et d’accéder aux composantes de ces valeurs grace au filtrage des motifs. Types énumérés # type couleur = Bleu | Vert | Rouge;; type couleur = Bleu | Vert | Rouge # let valcoul = function Bleu -> 1 | Rouge -> 2 | Vert -> 3;; val valcoul : couleur -> int = <fun> # valcoul Rouge;; - : int = 2 Les noms des constructeurs commencent par une majuscule. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les types énumérés février 2013 77 / 131 S. Guerrini (LIPN - Paris 13) Somme de types Programmation Fonctionnelle Les types énumérés La somme de types ou types variants février 2013 78 / 131 Somme de types Constructeurs et motifs La somme de deux types correspond à l’union disjointe. # type int_ou_bool = Int of int | Bool of bool;; type int_ou_bool = Int of int | Bool of bool # Int 4;; - : int_ou_bool = Int 4 # Bool true;; - : int_ou_bool = Bool true L’utilisation des expression de ce type se fait par pattern matching . # let f = function Bool x -> if x then 0 else 1 | Int x -> if x < 0 then x else x + 2;; val f : int_ou_bool -> int = <fun> Les constructuer ne sont pas des fonctions # Int;; Characters 0-3: Int;; ^^^ Error: The constructor Int expects 1 argument(s), but is applied here to 0 argument(s) # Int false;; Characters 4-9: Int false;; ^^^^^ Error: This expression has type bool but an expression was expected of type int S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 79 / 131 # # # - f : f : f : (Int 3);; int = 5 (Int (-2));; int = -2 (Bool true);; int = 0 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 80 / 131 Les types énumérés Types énumérés paramétrés Les types énumérés Types énumérés paramétrés Exemples On peut aussi definir des types énumérés paramétrés. Les constructeur de ces types prennent des élement de types paramétrés. Une fonction qui échange les types des elements des constructeur. # let f = function Left x -> Right x | Right x -> Left x;; val f : (’a, ’b) sum -> (’b, ’a) sum = <fun> # type (’a, ’b) sum = Left of ’a | Right of ’b;; type (’a, ’b) sum = Left of ’a | Right of ’b # # # - Left 3;; : (int, ’a) sum = Left 3 Right false;; : (’a, bool) sum = Right false Right [(1,2)];; : (’a, (int * int) list) sum = Right [(1, 2)] # # - Programmation Fonctionnelle Les types énumérés # let g = function Left x -> Right x | Right x -> Left (x+1);; val g : (’a, int) sum -> (int, ’a) sum = <fun> # # février 2013 81 / 131 Par rapport au résultat, il n’y a aucun information dans une expression de type unit On sait déjà que sont valeur sera () Le type unit et la valeur () sont une sorte d’entités vides. Ils ne sont pas l’équivalent de ce que de void ou NULL dans d’autres langages. Utilisation principales du type unit : I S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 82 / 131 Le type unit Fonctions sans arguments # () ;; - : unit = () I (Left "hello");; (int, string) sum = Right "hello" (Right 3);; (int, ’a) sum = Left 4 Les types énumérés Le type unit est un type enumeré predefini qui contient une unique valeur : I g : g : Le type unit Le type unit I f (Left "hello");; : (’a, string) sum = Right "hello" f (Right 3);; : (int, ’a) sum = Left 3 Une autre fonction qui échange le rôle des constructeurs. # let left_ou_right = function Left x -> true | Right x -> false;; val left_ou_right : (’a, ’b) sum -> bool = <fun> # left_ou_right (Left true);; - : bool = true # left_ou_right (Left "hello");; - : bool = true # left_ou_right (Right "hello");; - : bool = false S. Guerrini (LIPN - Paris 13) Types énumérés paramétrés C’est le type de l’input d’une fonction qui ne prennent pas d’argument particulier. C’est le type de retour d’une fonction n’ayant pas une réelle valeur de retour. On veut écrire une fonction qui ajoute 1 à un entier lis de l’input. La fonction read_int: unit -> int prend un entier de l’input standard et le renvoie comme valeur. # let f = read_int () + 1;; # let g () = read_int () + 1;; 3 <---- input val f : unit -> int = <fun> val int : 4 I f est une expression et elle est évaluée tout de suite : on lit la valeur 3 de l’input et on obtient la valeur 4 I les évaluations ultérieurs de f donneront toujours 4 I g est une fonction et son corps est évalué seulement lorsqu’on l’appel Quand et comment-on évalue la fonction g ? I I I I I Moralement, g est une fonction de 0 arguments. Formellement, elle est une fonction sur le type unit La seule valeur d’input possible, c’est pourtant (). Le corps de la fonction est évalué a tout appel de la fonction. Tout appel de la fonction g a la forme g (). # g ();; 3 <---- input - : int = 4 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 83 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 84 / 131 Les types énumérés Le type unit Les types énumérés Le type unit Fonctions qui ne renvoie pas une valeur (procédures) Le type unit est utilisé par et en combinaison avec les primitives impératives de OCaml. Types récursifs Si la fonction g: unit -> int ne dépend pas de l’input ou d’autres valeurs qui peuvent changer I I la fonction aura toujours la même valeur ce n’est pas la peine de l’exécuter tout le fois Le type unit est utilisé comme type du résultat d’une fonction qui I I réalise une action (de type impératif) qui dépend de l’état ou de l’input mais ne renvoie aucune valeur particulière. # print_endline "OCaml";; OCaml <---- output - : unit = () print_endline: string -> unit S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les types récursifs février 2013 85 / 131 S. Guerrini (LIPN - Paris 13) Entiers en notation unaire Programmation Fonctionnelle Les types récursifs Types récursifs : entiers en notation unaire février 2013 86 / 131 février 2013 88 / 131 Les listes Types récursifs : listes d’entiers On utilise les constructeurs pour définir des types récursifs. Une définition alternative des entiers (notation unaire !). # type entier = Zero | Succ of entier;; type entier = Zero | Succ of entier On peut redéfinir le type liste d’entiers avec # type liste_entiers = Nil | Cons of entier * liste_entiers;; type liste_entiers = Nil | Cons of entier * liste_entiers # let rec somme = function Nil -> Zero | Cons (x,l) -> add x (somme l);; val somme : liste_entiers -> entier = <fun> Les fonctions successeur et prédécesseur. # let succ x = Succ x;; val succ : entier -> entier = <fun> # let pred = function Zero -> Zero | Succ x -> x;; val pred : entier -> entier = <fun> Dans cet exemple, I I La somme de deux entiers unaires Nil est un constructeur d’arité 0 Cons un constructeur d’arité 2. # let rec add x = function Zero -> x | Succ y -> Succ (add x y);; val add : ’a -> entier -> ’a = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 87 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les types récursifs Les listes Les types récursifs Types récursifs : listes polymorphes Les listes Types récursifs : listes hétérogènes On peut également recréer le type polymorphe des listes en définissant le type paramétré suivant. On peut utiliser les types paramétrés pour créer par exemple des listes pouvant contenir des valeurs de types di↵érents. # type ’a liste = Nil | Cons of ’a * (’a liste);; type ’a liste = Nil | Cons of ’a * ’a liste # let rec liste_fold_right f l b = match l with Nil -> b | Cons (x,reste) -> f x (liste_fold_right f reste b);; val liste_fold_right : (’a -> ’b -> ’b) -> ’a liste -> ’b -> ’b = <fun> # type (’a,’b) liste = Nil | Acons of ’a * (’a,’b) liste | Bcons of ’b * (’a,’b) liste;; type (’a, ’b) liste = Nil | Acons of ’a * (’a, ’b) liste | Bcons of ’b * (’a, ’b) liste S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les types récursifs février 2013 89 / 131 S. Guerrini (LIPN - Paris 13) Les listes Programmation Fonctionnelle Pattern Martching février 2013 90 / 131 février 2013 92 / 131 Patterns Matching Pattern matching (Filtrage de motifs) S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Un exemple déjà vu. # let rec filter l p = match l with [] -> [] | x :: reste -> let l = filter reste p in if p x then (x :: l) else l;; val filter : ’a list -> (’a -> bool) -> ’a list = <fun> février 2013 91 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Pattern Martching Patterns Pattern Martching Patterns La construction Les di↵érentes formes de motifs vues jusqu’ici sont les suivantes. I Si e, e1,...,en sont des expressions et si p1,. . . ,pn sont des motifs, alors match e with p1 -> soit un élément d’un type de base prédéfini (bool, int, string, ...), soit un constructeur d’arité 0, comme par exemple le constructeur []). Si C est un constructeur (le constructeur binaire ; des listes :: entre bien sûr dans cette catégorie même si, dans ce cas, la notation est infixe) d’arité n1 et si p1, . . . , pn sont des motifs, alors p = C (p1,...,pn) est un motif ; Si l1, . . . , lk sont certains des noms de champs d’un type enregistrement et si p1, . . . , pk sont des motifs, alors p = l1 = p1;...;lk = pk est un motif. Programmation Fonctionnelle Pattern Martching février 2013 e1 | ··· | pn -> en est une expression, qui est equivalente à Toute variable est un motif. S. Guerrini (LIPN - Paris 13) match La construction match a la signification suivante. Toute constante est un motif. Etant entendu qu’une constante est I La construction match 93 / 131 (function p1 -> e1 | ··· | pn -> en) e Inversement, function p1 -> e1 |···| pn -> en peut s’ecrire function x = (match x with p1 -> e1 |···| pn -> en) S. Guerrini (LIPN - Paris 13) La construction match Programmation Fonctionnelle Pattern Martching Pattern matching non exhaustif février 2013 94 / 131 février 2013 96 / 131 La construction match Le pattern universel Les patterns peuvent ne pas couvrir tous les cas possibles : # let f l = match l with [] -> 0;; Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: _::_ val f : ’a list -> int = <fun> Il existe un motif qui filtre n’importe quelle valeur. La valeur filtrée est perdue après le filtrage. Néanmoins, la fonction est definie sur les cas filtrés par un pattern : # f [];; - : int = 0 Mais, elle n’est pas définie sur les cas qui ne correspondent à aucun pattern : # let rec meme_lg l1 l2 = match (l1,l2) with ([],[]) -> true | (_::reste1, _::reste2) -> meme_lg reste1 reste2 | (_, _) -> false val meme_lg : ’a list -> ’b list -> bool = <fun> # f [1];; Uncaught exception: Match_failure ("", 10, 35). S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 95 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Pattern Martching La construction match Pattern Martching La construction match Linéarité Les motifs doivent être linéaires, c’est-à-dire qu’une même variable ne peut apparaı̂tre qu’au plus une fois dans un motif. # let eg = function (x,x) -> true | _ -> false;; This variable is bound several times in this matching Les arbres La construction as permet de nommer un sous-motif d’un motif. # let f = function [] -> [] | x::[] -> [] | x::((y::m) as l) -> m@l;; val f : S. Guerrini (LIPN - Paris 13) a list -> a list = <fun> Programmation Fonctionnelle Les arbres février 2013 97 / 131 S. Guerrini (LIPN - Paris 13) Arbres binaires Programmation Fonctionnelle Les arbres Arbres binaires 98 / 131 Arbres binaires Fonctions récursives sur des arbres binaires Arbres binaires avec des étiquettes entiers sur les noeud. Une fonction qui compte les noeuds d’un arbre : # type arbre = Vide | Noeud of int * arbre * arbre;; # let rec taille = function Vide -> 0 | Noeud (n, t1, t2) -> 1 + taille t1 + taille t2;; val taille : ’a arbre -> int = <fun> et avec des étiquettes de type quelconque (mais toutes du même type) # type ’a arbre = Vide | Noeud of ’a * (’a arbre) * (’a arbre);; Un exemple d’arbre binaire : Une fonction qui somme les étiquettes d’un arbre d’entiers : let ex = Noeud (3, Noeud (2, Vide, Noeud (5, Vide, Vide)), Noeud (1, Vide, Noeud (2, Vide, Vide)));; val ex : int arbre = Noeud (3, Noeud (2, Vide, Noeud (5, Vide, Vide)), Noeud (1, Vide, Noeud (2, Vide, Vide))) S. Guerrini (LIPN - Paris 13) février 2013 Programmation Fonctionnelle # let rec somme = function Vide -> 0 | Noeud (n, t1, t2) -> n + somme t1 + somme t2;; val somme : int arbre -> int = <fun> Une fonction qui élimine les sous-arbres qui vérifient une condition donnée : let rec tranche p = function | Vide -> Vide | Noeud (x, t1, t2) as t -> if (p t) then Vide else Noeud (x, tranche p t1, tranche p t2);; février 2013 99 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 100 / 131 Les arbres Arbres binaires Les arbres Fonctionnelles d’itérations Parcours des arbres binaires On peut définir des fonctionelles d’itérations semblables à celles (prédéfinies) sur les listes (fold). La fonction suivante est l’équivalent d’un fold right # let fold_arbre f a = let rec fold = function Vide -> a | Noeud (x, t1, t2) -> f x (fold t1) (fold t2) in fold;; val fold_arbre : (’a -> ’b -> ’b -> ’b) -> ’b -> ’a arbre -> ’b = <fun> On peut écrire une version de infix de meilleure complexité à l’aide d’une variable appelée accumulateur. let infix t = let rec infix_accu t accu = match t with | Vide -> accu | Noeud (x, t1, t2) -> infix_accu t1 (x :: infix_accu t2 accu) in infix_accu t [];; la fonction qui somme les étiquettes d’un arbre # let somme t = fold_arbre (fun x y1 y2 -> x + y1 + y2) 0 t;; val somme : int arbre -> int = <fun> I La fonction infix suivante donne la projection verticale d’un arbre ou l’ordre interne ou encore infixé. # let rec infix = function | Vide -> [] | Noeud (x, t1, t2) -> (infix t1) @ (x :: infix t2);; val infix : ’a arbre -> ’a list = <fun> A l’aide de fold arbre on peur réécrire I Arbres binaires et la fonction qui cherche la plus grande étiquette # let greatest t = fold_arbre (fun x y1 y2 -> max x (max y1 y2)) min_int t;; val greatest : int arbre -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les arbres février 2013 101 / 131 S. Guerrini (LIPN - Paris 13) Arbres binaires Programmation Fonctionnelle Les arbres Variantes février 2013 102 / 131 Expressions arithmétiques Expressions arithmétiques Des arbres avec des étiquettes sur les feuilles seulement : # type ’a arbre = Feuille of ’a | Noeud of ’a arbre * ’a arbre;; On peut définir un type pour les opérateurs arithmétiques : Un exemple d’arbre # type opArith = Somme | Mult;; # Noeud (Noeud (Feuille 2, Noeud (Feuille 3, Feuille 1)), Noeud (Feuille 0, Noeud (Feuille (-1), Feuille 4)));; - : int arbre = ... Une expression sera un arbre avec des entiers aux feuilles et des opérateurs arithmétiques aux autres noeuds : Des arbres avec des étiquettes de types di↵érents sur les feuilles et sur les autres noeuds : # type (’a, ’b) arbre = Feuille of ’a | Noeud of ’b * (’a, ’b) arbre * (’a, ’b) arbre;; Un exemple d’arbre # Noeud (’+’, Noeud (’*’, Feuille 2, Noeud (’+’, Feuille 3, Feuille 1)), Noeud (’*’, Feuille 0, Noeud (’+’, Feuille (-1), Feuille 4)));; - : (int, char) arbre = ... let aex = Noeud (Somme, Noeud (Mult, Feuille 2, Noeud (Somme, Feuille 3, Feuille 1)), Noeud (Mult, Noeud (Somme, Feuille (-1), Feuille 4), Feuille 0));; val aex : (int, opArith) arbre = ... Donc le type des expressions sera défini par : # type expr = (int, opArith) arbre;; qui représente une expression arithmétique. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 103 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 104 / 131 Les arbres Expressions arithmétiques Les arbres Evaluation des expressions arithmétiques Expressions arithmétiques Arbres syntaxiques Une autre représentation des expressions : les opérateurs deviennent des constructeurs de l’arbres. type expr = Val of int | Sum of expr * expr | Prod of expr * expr;; Pour calculer la valeur d’une expression, on écrit une fonction récursive : let rec eval_expr = function Feuille n -> n | Noeud (Somme, t1, t2) -> eval_expr t1 + eval_expr t2 | Noeud (Mult, t1, t2) -> eval_expr t1 * eval_expr t2;; Un exemple d’expression : # let ex = Sum (Prod (Val Sum Prod (Sum Val val ex : expr = ... Une autre écriture de la fonction d’évaluation : # let rec eval_expr = function Feuille n -> n | Noeud (op, t1, t2) -> let (v1, v2) = (eval_expr t1, eval_expr t2) in match op with Somme -> v1 + v2 | Mult -> v1 * v2;; val eval_expr : (int, opArith) arbre -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les arbres L’avantage de ce type de représentation est qu’on peut ajouter facilement des opérateur non-binaires, par exemple, l’opérateur unarie Neg n qui donne l’opposé n de son argument et l’opérateur Exp n qui donne 2n . type expr = Val of int | Sum of expr * expr | Prod of expr * expr | Neg of expr | Exp of expr;; février 2013 105 / 131 Programmation Fonctionnelle Les arbres février 2013 106 / 131 Expressions arithmétiques avec des variables La valeur des variables : l’environnement Pour évaluer une expression qui contient des variables, on a besoin de connaitre les valeurs des variables. On ajoute la possibilité que les feuilles soient des variables : On a besoin de ce qu’on appelle l’environnement. type expr = Val of int | Var of char | Sum of expr * expr | Prod of expr * expr;; L’environnement est une fonction qui associe une valeur aux variables. Par exemple : let env = function ’x’ -> 2 | ’y’ -> 3 | ’z’ -> -1 | _ -> 0;; Un exemple d’expression avec des variables : S. Guerrini (LIPN - Paris 13) S. Guerrini (LIPN - Paris 13) Expressions arithmétiques avec des variables Expressions arithmétiques avec des variables # let ex2 = Sum (Prod (Var Sum Prod (Sum Var val ex2 : expr = ... 2, (Val 3, Val 1)), (Val (-1), Val 4), 0));; ’x’, (Val 3, Var ’z’)), (Var ’y’, Val 4), ’x’));; Comme ça, on donne une valeur 0 à toutes variables qu’on n’utilises pas. Mais, on peut aussi laisser ces valeurs indéfinies : let env ’x’ | ’y’ | ’z’ Programmation Fonctionnelle février 2013 107 / 131 = function -> 2 -> 3 -> -1;; S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 108 / 131 Les arbres Expressions arithmétiques avec des variables Les arbres La fonction d’évaluation Expressions arithmétiques avec des variables Variables non-définies La fonction d’évaluation des expressions dans un environnement : let rec eval_expr env = function Val n -> n | Var x -> env x | Sum (t1, t2) -> eval_expr env t1 + eval_expr env t2 | Prod (t1, t2) -> eval_expr env t1 * eval_expr env t2;; Une version qui lève une exception si on évalue une variable non-définies : Une autre version qui élimine le passage du paramètre env dans les appels récursif : let eval_expr env = let rec eval = function Val n -> n | Var x -> env x | Sum (t1, t2) -> eval t1 + eval t2 | Prod (t1, t2) -> eval t1 * eval t2 in eval;; S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les arbres février 2013 109 / 131 # let eval_expr = let env = function x -> failwith ("Undef var ’" ^ Char.escaped x ^ "") in let rec eval = function Val n -> n | Var x -> env x | Sum (t1, t2) -> eval t1 + eval t2 | Prod (t1, t2) -> eval t1 * eval t2 in eval;; val eval_expr : expr -> int = <fun> S. Guerrini (LIPN - Paris 13) Expressions arithmétiques avec des variables Programmation Fonctionnelle Les arbres A↵ectation d’une valeur à une variable locale : le constructeur Let février 2013 110 / 131 Expressions arithmétiques avec des variables A↵ectation d’une valeur à une variable locale : évaluation du Let Au moment de l’évaluation du Let on ajoute une nouvelle variable à l’environnement avec la valeur donnée : On ajoute une construction Let (x,n,e) qui associe un entier n à une variable x dans l’évaluation di une expression e. On étend la définition du type des expression avec un constructeur Let : type expr = Val of int | Var of char | Let of char * int * expr | Sum of expr * expr | Prod of expr * expr;; La portée de l’a↵ection de la valeur n à la variable x est limitée à l’expression e (comme dans OCaml). # let rec eval_expr env = let rec eval = function Val n -> n | Var x -> env x | Let (c, n, t) -> let newenv x = if (x = c) then n else env x in eval_expr newenv t | Sum (t1, t2) -> eval t1 + eval t2 | Prod (t1, t2) -> eval t1 * eval t2 in eval;; val eval_expr : (char -> int) -> expr -> int = <fun> L’environnement newenv est utilisé seulement dans l’évaluation de l’expression t du Let. Les autres cas sont tous évalués dans l’environnement env. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 111 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 112 / 131 Les arbres Expressions arithmétiques avec des variables Les arbres A↵ectation d’une expression à une variable locale Arbres généraux Arbres généraux La valeur de l’a↵ectation devient une valeur : Arbres avec un nombre de branche variable type expr = Val of int | Var of char | Let of char * expr * expr | Sum of expr * expr | Prod of expr * expr;; # type ’a arbre = Noeud of ’a * ’a arbre list;; Un exemple d’arbre : Au moment de l’évaluation du Let il faut d’abord évaluer l’expression qui donne la valeur de la variable dans l’environnement env. # let rec eval_expr env = let rec eval = function Val n -> n | Var x -> env x | Let (c, v, t) -> let newenv x = if (x = c) then (eval v) else (env x) in eval_expr newenv t | Sum (t1, t2) -> eval t1 + eval t2 | Prod (t1, t2) -> eval t1 * eval t2 in eval;; val eval_expr : (char -> int) -> expr -> int = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Les arbres let ex = Noeud (3, [ Noeud (2, [ Noeud (5, []); Noeud (2, [ Noeud Noeud Noeud (4, []); Noeud (1, [ Noeud Noeud (1, [ Noeud (2, []); Noeud (1, [ Noeud (3, []); (7, []) ]); (3, []) ]) ]), (3, [ Noeud (4, []); Noeud (1, []); Noeud (7, []) ]) ]) ]) ]);; val ex : int arbre = ... février 2013 113 / 131 S. Guerrini (LIPN - Paris 13) Arbres généraux Programmation Fonctionnelle Les arbres février 2013 114 / 131 février 2013 116 / 131 Arbres généraux Fonctions récursives sur des arbres Une fonction qui compte les noeuds d’un arbre : let rec compte (Noeud (x, ls)) = let rec compte_ls = function | [] -> 0 | t :: r -> compte t + compte_ls r in 1 + compte_ls ls;; Input/Output Une fonction qui élimine les sous-arbres qui vérifient une condition donnée : # let let and | | tranche p = rec tranchep (Noeud (x, ls)) = Noeud (x, tranchep_ls ls) tranchep_ls = function [] -> [] t :: r -> if (p t) then tranchep_ls r else tranchep t :: tranchep_ls r in tranchep;; val tranche : (’a arbre -> bool) -> ’a arbre -> ’a arbre = <fun> S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 115 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Output Ecrire dans la sortie standard Output Sortie de valeurs de base La fonction printf OCaml prévoit des fonctions de sortie vers stdout pour tous les types de base. # # # # - Le module Printf définie une fonction printf qui 1 print_int;; : int -> unit = <fun> print_float;; : float -> unit = <fun> print_string;; : string -> unit = <fun> print_char;; : char -> unit = <fun> 2 I I I %i dénote un entier %s une chaı̂ne de caractères ... Un exemple : # print_newline;; - : unit -> unit = <fun> L’interpréteur sait afficher des valeurs de type autre que les types de bases (e.g., listes et nuplet). Par contre, si on veut sortir une telle valeur vers stdout alors il faut écrire une fonction pour le faire. Programmation Fonctionnelle Output février 2013 117 / 131 # Printf.printf "entier: %i\nstring: %s\n" 4 "toto";; entier: 4 string: toto - : unit = () Similaire à printf dans C, C++, Java, ... Le typage de printf et des autres fonctions d’output (input) formaté est particulier : I le nombre et les types des arguments dépendent du premier argument : la chaı̂ne du format d’output S. Guerrini (LIPN - Paris 13) Ecrire dans un canal d’output février 2013 118 / 131 Ecrire dans un canal d’output Fonctions d’écriture Tout processus UNIX a trois canaux de communication (voir un cours de Systèmes) : Il y a des variantes des fonction d’écriture vues précédemment pour écrire I stdin : entrée normale du processus, normalement associée au clavier. Peut aussi être une redirection d’un tuyau ou d’un fichier. I stdout : sortie normale du processus, normalement associée à l’écran. Peut aussi être redirigée vers un tuyau ou un fichier. stderr : sortie pour les messages d’erreur. Normalement confondue avec stdout et associée à l’écran, mais peut aussi être redirigée. Programmation Fonctionnelle Programmation Fonctionnelle Output Canaux de communication S. Guerrini (LIPN - Paris 13) prend en premier argument une chaı̂ne qui décrit le format, puis tant d’arguments que demandé par le format. Dans le format, La sortie vers stdout n’est pas e↵ectuée tout de suite : il y a un tampon. La fonction print newline force la sortie du contenu du tampon de sortie. S. Guerrini (LIPN - Paris 13) Ecrire dans la sortie standard février 2013 119 / 131 dans un canal de sortie quelconque dans une chaı̂ne de caractères. Il y a aussi des fonctions analogues pour la sortie vers stderr. Attention : distinguer toujours stdout et stderr : un utilisateur peut avoir besoin de séparer la sortie normale des messages d’erreur. I Fonctions : prerr int, etc. (voir le manuel) S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 120 / 131 Output Ecrire dans un fichier Output Ouverture et fermeture d’un fichier d’output Fonctions d’écriture Fonction open out pour ouvrir un fichier I I I I Type : string -> out channel. Si le fichier n’existe pas il est créé. Le résultat est un canal d’output : pour écrire dans le fichier il faudra écrire dans cet canal. open out peut lever une exception Sys error, F par exemple, quand on n’a pas les droits nécessaires pour créer ou ouvrir le fichier. Programmation Fonctionnelle Input février 2013 121 / 131 Il n’y a pas de output int En revanche, il y a une variante de printf, du nom fprintf, pour écrire dans un canal. La sortie vers un canal est tamponnée. Fonctions flush pour vider le tampon d’en canal et flush all pour vider tous les tampons de sortie. S. Guerrini (LIPN - Paris 13) Canal d’entrée standard février 2013 122 / 131 Lire dans un fichier Lire dans un fichier La fonction read line attend sur stdin une ligne terminée par retour-chariot, et envoie comme résultat le contenu de cette ligne (sous forme d’un string) mais sans le retour-chariot. Il y a également read int et read float. Le module Scanf permet de lire des lignes dans un format précis (analogue à Printf). Problème dans le cas du mode emacs : la boucle attend toujours ;; pour terminer une entrée. Programmation Fonctionnelle Programmation Fonctionnelle Input Entrée par le canal d’input standard S. Guerrini (LIPN - Paris 13) Le premier argument est le canal. I Type : out channel -> unit S. Guerrini (LIPN - Paris 13) Fonctions output string, output char pour écrire dans un canal de sortie. I Fonction close out pour fermer un fichier I Ecrire dans un fichier février 2013 123 / 131 Fonction open in pour ouvrir un fichier en lecture. I open in lève l’exception Sys error si le fichier ne peut pas être ouvert. F Par exemple, parce qu’il n’existe pas. Fonction close in pour fermer le canal. Fonction input line pour lire une ligne complète. I input line lève l’exception End of file quand on est à la fin du fichier. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 124 / 131 Input Lire dans un fichier Exceptions Exceptions Exceptions Nous allons décrire le mécanisme d’exception d’Objective Caml. # let tete = function [] -> failwith "tete" | x::_ -> x;; val tete : ’a list -> ’a = <fun> # failwith;; - : string -> ’a = <fun> # tete [];; Uncaught exception: Failure "tete". Les exceptions S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Exceptions février 2013 125 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Exceptions Exceptions Le type des exceptions février 2013 126 / 131 Exceptions Levée d’une exception La levée d’une exception se fait à l’aide de la fonction raise En OCaml, les exceptions appartiennent à un type énuméré prédéfini exn. I Le constructeur Failure est un constructeur d’exception avec une string comme argument # Failure "hello !";; - : exn = Failure "hello !" Remarquez le type de raise # raise;; - : exn -> ’a = <fun> La syntaxe pour définir de nouvelles exceptions est # exception Erreur;; exception Erreur # Erreur;; - : exn = Erreur I I Le type des exceptions a une particularité les autres types énumérés n’ont pas cette possibilité. L’opérateur exception étend l’ensemble des valeurs de exn en déclarant un nouveau constructeur. On définit comme cela des nouvelles exceptions. Programmation Fonctionnelle Pourtant, pour tout exception ex, l’expression raise ex a un type ’a quelconque. Cela permets d’utiliser raise n’importe ou dans une expression, sans casser le type de l’expression. Mais comme on fait une fonction à renvoyer une valeur de type ’a pour tout type ’a, même ceux qui ne sont pas encore défini ? exn est un type énuméré extensible S. Guerrini (LIPN - Paris 13) # let tete = function [] -> raise (Failure "tete") | x::_ -> x;; val tete : ’a list -> ’a = <fun> février 2013 127 / 131 En fait raise ne termine jamais régulièrement ! raise ne renvoie jamais une valeur, mais termine toujours en levant une exception. Pourtant, raise n’as pas besoin d’une valeur de type ’a. Le fait que raise est une fonction avec type d’output ‘a est un artifice qui permet de l’utiliser sans casser l’inférence des types. S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 128 / 131 Exceptions Exceptions Exceptions Des exemples Exceptions Capture des exceptions # let tete = function [] -> raise Erreur | x::_ -> x;; val tete : ’a list -> ’a = <fun> En cas de levée d’une exception, la capture de l’exception permet de continuer un calcul. # exception Trouve of int;; exception Trouve of int On peut utiliser les captures d’exception comme un véritable style de programmation. # Trouve 5;; - : exn = Trouve 5 La construction try-with permet de le réaliser les captures. Les di↵érentes étapes à l’exécution sont les suivantes. I # raise (Trouve 7);; Uncaught exception: Trouve 7. I I # exception Erreur_fatale of string;; exception Erreur_fatale of string I Essayer de calculer l’expression. Si aucune erreur n’est déclenchée, on retourne l’expression. Si cette évaluation déclenche une erreur qui tombe dans un cas de filtrage, alors on retourne la valeur correspondante de la clause sélectionnée par le filtrage. En cas d’échec sur le filtrage, la valeur exceptionnelle est propagée. # raise (Erreur_fatale "cas non prevu");; Uncaught exception: Erreur_fatale "cas non prevu". S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle Exceptions février 2013 129 / 131 Exceptions Try-with Une fonction qui cherche une valeur qui vérifie un prédicat p : ’a -> bool # let rec ch p n = function [] -> raise Erreur | x::l -> if (p x) then raise (Trouve n) else ch p (n+1) l;; val ch : (’a -> bool) -> int -> ’a list -> ’b = <fun> La fonction lève l’exception Erreur si on ne trouve pas de valeurs, ou l’exception Trouve n quand on trouve une valeur tel que p n est vrai. On utilise try-with pour gérer les exceptions : # let cherche p l = try (ch p 1 l) with Erreur -> raise (Failure "cherche: rien trouve") | Trouve n -> n;; val cherche : (’a -> bool) -> ’a list -> int = <fun> I Si on trouve une valeur : # cherche pair [1;5;3;4;6;7];; - : int = 4 I Si on ne trouve pas de valeur : # cherche pair [1;5;3];; Uncaught exception: Failure "cherche: rien trouve". S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 131 / 131 S. Guerrini (LIPN - Paris 13) Programmation Fonctionnelle février 2013 130 / 131