USTL - Licence info 3ème année 2008-2009 Expression Logique et Fonctionnelle . . . Évidemment Programmation fonctionnelle octobre 2008 2 Chapitre 1 Programmation fonctionnelle 1.1 Programmation fonctionnelle Lisp Premier langage informatique fonctionnel : (n des années 1950). Base théorique : λ-calcul inventé par Alonzo Church dans les années 1930 pour dénir la notion de calculabilité. Nombreux langages fonctionnels : , , , , , . . . qui se distinguent selon diérents critères Sans ou avec eets de bord : un langage fonctionnel sans eet de bord, ou langage pur, est un langage où l'aectation n'existe pas. Les langages purs restent assez rares, citons parmi eux. , , la famille des langages sont impurs. Typés dynamiquement ou statiquement : les arguments d'une fonction doivent être d'un certain type pour que l'évaluation d'une application puisse avoir lieu. La vérication du type, ou typage, peut se faire à l'exécution du programme, on parle alors de typage dynamique. C'est le cas de langages comme ou . Inversement, le typage peut avoir lieu avant l'exécution du programme, lors de la compilation, le typage est alors qualié de statique. C'est le cas des langages de la famille . Distinction entre programmation impérative et programmation fonctionnelle : Notions de base en programmation impérative : état d'une machine, instructions, temps (séquence), itération. Nécessité d'avoir une connaissance minime du modèle de machine sur laquelle on programme (modèle de Von Neumann). Notions de base = valeurs, expressions et fonctions. L'exécution d'un programme est l'évaluation d'une ou plusieurs expressions, expressions qui sont souvent des applications de fonctions à des valeurs passées en paramètre1 . Pas besoin de modèle de machine. Lisp Scheme Haskell SML OCaml Lisp Scheme Lisp ML Haskell Scheme ML 1.1.1 Caml OCaml ML Langage utilisé : , dialecte de la famille . C'est un langage développé à l'INRIA2 (http ://caml.inria.fr). Ce langage ore un noyau fonctionnel ; des structures de contrôle impératives, ainsi que des types de données mutables ; la possibilité de programmation par objets. Il est utilisé dans l'enseignement (classes prépa, universités) ; dans l'industrie (CEA, France Telecom, EDF) ; et il est particulièrement adapté au prototypage d'applications ; aux preuves de programmes (cf Coq) ; compilation et interprétation. 1 On dit aussi programmation applicative 2 Institut National de Recherche en Informatique et Automatique 3 4 CHAPITRE 1. 1.2 PROGRAMMATION FONCTIONNELLE Premiers pas 1.2.1 Phrases du langage OCaml Un programme fonctionnel écrit en est une suite de phrases, déclarations de variables ou de types, ou bien expressions à évaluer. L'exécution d'un programme consiste en l'évaluation de toutes les phrases. Les phrases du langage sont terminées par un double point-virgule ( ; ;). Ci-dessous quelques exemples de phrases évaluées par l'interprète du langage (Le symbole # est le prompt de la boucle d'interaction, ce qui suit est la phrase à évaluer. Les lignes débutant par le symbole - sont les résultats de l'évaluation de la phrase). Objective Caml version 3.09.2 # # # # # # # # # # # # # # # - 132 ;; : int = 132 2*(45+10) ;; : int = 110 7/4 ;; : int = 1 7 mod 4 ;; : int = 3 2. ;; : float = 2. 2. +. 3. ;; : float = 5. 7./.4. ;; : float = 1.75 float_of_int 2 ;; : float = 2. true ;; : bool = true true & false ;; : bool = false 1 = 2-1 ;; : bool = true "Hello" ;; : string = "Hello" "Hello " ^ "le monde " ;; : string = "Hello le monde " ’A’ ;; : char = ’A’ int_of_char ’B’ ;; : int = 66 1.2.2 Types de base OCaml ML , comme tous les langages de la famille , est un langage fortement typé, à typage statique. Avant d'évaluer une phrase, le type de l'expression est calculé, c'est l'inférence de type : contrairement à des langages comme , , , le programmeur n'a pas besoin de préciser le type de ces expressions, variables ou fonctions. Le type d'une expression est calculé à partir de ses composants. Si l'inférence de type échoue, l'expression n'est pas évaluée. Si en revanche elle réussit, l'expression peut être évaluée. Voici les types de base du langage : C Pascal Ada int : les entiers. L'intervalle du type int dépend de l'architecture de la machine utilisée. C'est l'intervalle [[−230 , 230 − 1]] sur les machines 32 bits. 1.2. 5 PREMIERS PAS Opérateur + * / mod signication addition soustraction multiplication division entière reste de la division entière oat : OCaml les nombres ottants. Ils respectent la norme IEEE 754. distingue les entiers des ottants : 2 est de type int, tandis que 2. est de type float, et ces deux valeurs ne sont pas égales. Même les opérateurs arithmétiques sur les ottants sont diérents de ceux sur les entiers. Opérateur signication +. addition -. soustraction multiplication *. /. division exponentiation ** On ne peut pas additionner un int et un float # 2 + 3. ;; Characters 4-6: 2 + 3. ;; ^^ This expression has type float but is here used with type int # 2. +. 3 ;; Characters 6-7: 2. +. 3 ;; ^ This expression has type int but is here used with type float Fonction float_of_int ceil floor sqrt exp log log10 ... string : signication conversion d'un int en float partie entière supérieure (plafond) partie entière inférieure (plancher) racine carrée exponentielle logarithme népérien logarithme décimal ... les chaînes de caractères (longueur limitée à 264 − 6). Opérateur ^ signication concaténation Fonction string_of_int int_of_string string_of_float float_of_string String.length char : signication conversion d'un conversion d'un conversion d'un conversion d'un longueur int en un string string en un int float en un string string en un float les caractères. Au nombre de 256, les 128 premiers suivent le code ASCII. Fonction signication char_of_int conversion d'un int en un char int_of_char conversion d'un char en un int Opérateur bool signication négation : les booléens pouvant prendre deux valeurs true et false. et séquentiel ou séquentiel Les opérateurs de comparaison sont polymorphes, ils sont utilisables pour comparer deux int, ou deux float, ou deux string,. . . . Mais ils ne permettent pas la comparaison d'un int et un float. not && ou & || ou or 6 CHAPITRE 1. Opérateur = <> < <= > >= unit : PROGRAMMATION FONCTIONNELLE signication égalité (structurelle) négation de = inférieur inférieur ou égal supérieur supérieur ou égal le type unit est un type ne possédant qu'une seule valeur notée (). Ce type est utilisé dans la partie impérative du langage , et par les diérentes procédures d'achage. Procédure signication print_int achage d'un entier print_float achage d'un ottant print_string achage d'une chaîne de caractères print_char achage d'un caractère print_newline passage à une nouvelle ligne OCaml # print_int 12 ;; 12- : unit = () # print_float 3.14 ;; 3.14- : unit = () # print_string"Hello" ;; Hello- : unit = () # print_char ’A’ ;; A- : unit = () # print_newline () ;; - : unit = () 1.2.3 Déclaration de variables En programmation fonctionnelle, une variable est une liaison entre un nom et une valeur. Les variables peuvent être globales, et elles sont alors connues de toutes les expressions qui suivent la déclaration, ou bien locales à une expression, et dans ce cas elles ne sont connues que de l'expression pour laquelle elles ont été déclarées. L'ensemble des variables connues d'une expression est appelé environnement de l'expression. La syntaxe d'une déclaration globale est de la forme let <nom> = <expr> ;; OCaml où < nom > est le nom de la variable (en les noms de variables doivent obligatoirement commencer par une lettre minuscule), et < expr > une expression décrivant la valeur de la variable. # let n = 12 ;; val n : int = 12 # n ;; - : int = 12 # 2 * n;; - : int = 24 # let m = 2 * n ;; val m : int = 24 # m + n;; - : int = 36 # m + p;; Characters 2-3: m+p;; ^ Unbound value p Pour être typable, et donc évaluable, toutes les variables d'une expression doivent être dénies dans l'environnement de l'expression, sous peine d'avoir le message d'erreur Unbound value. 1.2. PREMIERS PAS 7 On peut aussi déclarer simultanément plusieurs variables. let <nom1 > = <expr1 > and <nom2 > = <expr2 > ... and <nomn > = <exprn > ;; 1.2.4 Déclaration locale de variables Syntaxe des déclarations locales let <nom> = <expr1 > in <expr2 > ;; Cette forme syntaxique permet de déclarer la variable nommée < nom > dans l'environnement d'évaluation de l'expression < expr2 >. En dehors de l'environnement de cette expression, la variable < nom > n'existe pas. Il est possible de faire des déclarations simultanées devariables locales à une expression. let nom1 = expr1 and nom2 = expr2 ... and nomn = exprn in exprn+1 ;; # let pi = 3.141592 and r = 2.0 in 2. *. pi *. r ;; - : float = 12.566368 # pi ;; Characters 0-2: pi ;; ^^ Unbound value pi 1.2.5 Expression conditionnelle La syntaxe d'une expression conditionnelle est if <expr1 > then <expr2 > else <expr3 > ;; dans laquelle < expr1 > est une expression booléenne (type bool), < expr2 > et expr3 ont le même type. # if 1=1 then "égaux" else "non égaux" ;; - : string = "égaux" # if 1=2 then "égaux" else "non égaux" ;; - : string = "non égaux" # if 1=2 then 0 else "non égaux" ;; Characters 19-30: if 1=2 then 0 else "non égaux" ;; ^^^^^^^^^^^ This expression has type string but is here used with type int Remarques : OCaml OCaml en , la structure de contrôle conditionnelle est une expression et non une instruction, c'est la raison pour laquelle les deux expressions des parties then et else doivent être du même type. en , il est possible d'omettre la partie else d'une expression conditionnelle. Dans ce cas, elle est considérée comme étant la valeur () (type unit), et la partie then doit elle aussi être du type unit. 8 CHAPITRE 1. PROGRAMMATION FONCTIONNELLE 1.2.6 Fonctions OCaml En , comme dans tous les langages fonctionnels, les fonctions sont des valeurs comme toutes les autres. Une variable peut donc avoir une fonction pour valeur. # let racine_carree = sqrt ;; val racine_carree : float -> float = <fun> # racine_carree 2. ;; - : float = 1.41421356237309515 Type d'une valeur fonctionnelle Le type d'une fonction est déterminé par le type de son (ou ses) argument(s) et celui des valeurs qu'elle renvoie. Ainsi le type de la fonction sqrt est float -> float puisqu'elle prend un float pour paramètre et retourne un float. Dénition d'une fonction Le premier mot-clé permettant de dénir une fonction est function function <param> -> <expr > où param est le nom donné au paramètre formel, et expr est l'expression à évaluer lors d'un appel à la fonction. # function x -> x*x ;; - : int -> int = <fun> Dans l'exemple cidessus, l'évaluation de la phrase donne comme résultat une valeur fonctionnelle de type int -> int. Mais cette fonction n'est pas nommée. Il est bien entendu possible de nommer une fonction en déclarant une variable (globale ou non) # let carre = function x -> x*x ;; val carre : int -> int = <fun> Il existe une autre façon de déclarer une variable fonctionnelle, tout à fait équivalente à la première, et qui n'utilise pas le mot-clé function. let f x = expr ;; Par exemple, notre fonction carre peut être déclarée sous la forme # let carre x = x*x ;; val carre : int -> int = <fun> Appel à une fonction Pour appliquer une fonction f à une valeur v , il sut d'écrire le nom de la fonction suivi de la valeur f v # # - carre : int carre : int 2 ;; = 4 (2 + 2) ;; = 16 Attention aux parenthèses qui permettent de contrôler la priorité des opérateurs # carre 2+2 ;; - : int = 6 À noter que la syntaxe plus traditionnelle f (v ) est acceptable en Caml parce que l'expression (v) a pour valeur celle de v. 1.3. 9 EXERCICES # # - (2);; : int = 2 carre (2);; : int = 4 1.2.7 Déclarations récursives Envisageons la dénition de la fonction calculant la factorielle de l'entier passé en paramètre. Une expression récursive classique de n! est 0! = 1 n! = n × (n − 1)! ∀n ∈ N∗ On peut être tenté de traduire cette formulation par la déclaration # let fact n = if n=0 then 1 else n*fact(n-1) ;; mais on a tort car on obtient le message Characters 37-41: if n=0 then 1 else n*fact(n-1) ;; ^^^^ Unbound value fact qui indique que la variable fact apparaissant dans l'expression la dénissant n'est pas liée dans l'environnement de déclaration de la variable . . . fact. On tourne en rond. Pour remédier à cela, il faut utiliser la forme syntaxique let rec let rec <nom> = <expr > dans laquelle toute référence à la variable < nom > dans l'expression < expr > est une référence à la variable en cours de déclaration. # let rec fact n = if n=0 then 1 else n*fact(n-1) ;; val fact : int -> int = <fun> # fact 5 ;; - : int = 120 1.3 Exercices Exercice 1. Évaluations d'expressions Pour la session qui suit, donnez le type et la valeur de chacune des phrases let n1 = 10 ;; let n2 = n1+5 ;; let n1 = n2+6 ;; let vrai = true ;; vrai && false ;; vrai && true ;; let x = n1*n2 > n2*n2 ;; let y = x+1 ;; let z = 4 in z+2*z ;; let z = let z = 4 in z+2*z ;; let y = let x = z*z in x+2 ;; let message = let personnage = "La gardienne " and lieu = "du campus " and action = "dessine" in personnage ^ lieu ^ action ;; print_string (message^"\n") ;; 10 CHAPITRE 1. PROGRAMMATION FONCTIONNELLE Exercice 2. Un calcul de puissance En remarquant que a7 = a × a2 × (a2 )2 écrivez une expression calculant 137 en 4 multiplications de nombres entiers. Votre expression ne devra pas modier l'environnement. Exercice 3. Signe d'un entier Typez, puis écrivez la fonction qui à un entier positif associe 1, à un entier négatif associe -1, et à l'entier nul associe 0. Exercice 4. Ou exclusif Réalisez la fonction xor qui calcule le ou-exclusif de ces deux arguments. Exercice 5. Nombres de Fibonacci On rappelle que les nombres de Fibonacci sont dénis par récurrence par F0 = 0 F1 = 1 Fn+2 = Fn+1 + Fn ∀n ∈ N Réalisez la fonction fibo de type int → int telle que la valeur de fibo n soit égale à Fn . Exercice 6. Chaînes de caractères OCaml En , on peut utiliser la fonction sub, prédénie dans le module String, pour extraire une souschaîne d'une chaîne de caractères. String.sub s a b donne la sous-chaîne de s de longueur b démarrant à la position a (la position du premier caractère est 0). Par exemple : # String.sub "Expression Logique et Fonctionnelle Evidemment" 22 13;; - : string = "Fonctionnelle" La fonction length du module String donne la longueur d'une chaîne. # String.length "Expression Logique et Fonctionnelle Evidemment" ;; - : int = 46 Réalisez le prédicat est_palindrome qui teste si la chaîne passée en paramètre est un palindrome. Exercice 7. Fonction bien dénie ? Question 1. Quel est le type de la fonction f ? Quelles sont les valeurs qu'elle peut prendre ? let rec f x = if abs_float (x -. 1.) < 0.001 then true else f ((x +. 1.) /. 2.) ;; Question 2. Est-elle partout dénie ? Estimez en fonction de la valeur de x, le nombre d'appels récursifs. Chapitre 2 Ordre supérieur - Polymorphisme 2.1 Ordre supérieur L'un des points forts de la programmation fonctionnelle est de considérer les fonctions au même niveau que toute autre type de données. À ce titre il est possible d'eectuer des traitements sur des fonctions, et d'écrire des fonctions prenant des fonctions en paramètres et/ou produisant des fonctions en sorties. De telles fonctions dont qualiées de fonctions d'ordre supérieur. 2.1.1 Les fonctions d'arité supérieure à 1 Toute fonction d'arité supérieure à 1 peut être considérée en supérieur, à savoir une fonction d'arité 1 à valeur fonctionnelle. Par exemple la fonction OCaml comme une fonction d'ordre let add x y = x+y ;; est de type int -> int -> int. Cette fonction peut être vue comme une fonction d'arité 2 # add 1 2 ;; - : int = 3 La dénition précédente peut être remplacée par une autre forme équivalente let add = function x -> function y -> x+y ;; forme qui montre que la fonction add peut aussi être vue, et utilisée, comme une fonction d'arité 1 # add 1 ;; - : int -> int = <fun> On peut noter que la valeur de l'expression add 1 est du type fonctionnel int -> int, autrement dit le résultat est une fonction qui peut elle-même être appliquée à un entier. # (add 1) 2 ;; - : int = 3 En fait les deux expressions add 1 2 et (add 1) 2 expriment exactement la même chose. En , toutes les fonctions sont d'arité 1. Toute déclaration de la forme Caml let f x1 x2 ... xn = expr peut être déclarée par let f = function x1 -> function x2 -> ... -> function xn -> expr Les fonctions à plusieurs paramètres sont en fait des fonctions à un paramètre qui retournent une valeur d'un type fonctionnel. Le mot-clef fun permet une écriture plus concise let f = fun x1 x2 ... xn -> expr 11 12 CHAPITRE 2. ORDRE SUPÉRIEUR - POLYMORPHISME Par exemple let add = fun x y -> x+y ;; Le type de la fonction add est int -> (int -> int) qui est synonyme de int -> int -> int. Les types fonctionnels sont parenthésés à droite : σ1 → σ2 → . . . σn−1 → σn = σ1 → (σ2 → . . . (σn−1 → σn ) . . .) Attention, le type int -> int -> int est diérent du type (int -> int) -> int, comme on peut le constater ici # let carre x = x*x;; val carre : int -> int = <fun> # add carre ;; Characters 4-9: add carre ;; ^^^^^ This expression has type int -> int but is here used with type int Les types fonctionnels ne sont pas parenthésés à gauche. 2.1.2 Fonctionnelles De la même façon que le type de la valeur retournée par une fonction peut être une fonction, le type d'un paramètre peut aussi être une fonction. Une fonctionnelle est une fonction prenant une fonction en argument. Par exemple, la fonction # let demiValeurEnZero f = (f 0) / 2 ;; val demiValeurEnZero : (int -> int) -> int = <fun> est une fonctionnelle qui prend pour paramètre toute fonction f de type int -> int et qui retourne la valeur de f (0) 2 . # # - 2.2 demiValeurEnZero carre ;; : int = 0 demiValeurEnZero (add 2);; : int = 1 Polymorphisme 2.2.1 Fonctions polymorphes Une fonction peut être polymorphe. Par exemple, la fonction # let valeurEnZero f = f 0;; val valeurEnZero : (int -> ’a) -> ’a = <fun> est polymorphe car le type de la valeur qu'elle retourne est le même que le type du résultat de la fonction passée en paramètre. Le caractère polymorphe de cette fonction est marqué par la variable de type ’a. Une fonction peut aussi avoir des paramètres polymorphes. # let zero x = 0;; val zero : ’a -> int = <fun> 2.2. 13 POLYMORPHISME 2.2.2 Typage d'une expression fonctionnelle Voici comment calculer le type d'une expression fonctionnelle x 7→ expr dans un environnement Env . 1. Désigner par l'inconnue σ le type de x. 2. Désigner par l'inconnue τ le type de expr. L'expression focntionnelle a alors pour type σ → τ . 3. Typer chacune des sous-expressions de expr dans l'environnement Env . Des contraintes sur les inconnues σ et τ peuvent voir le jour. 4. En tenant compte de toutes les contraintes sur σ et τ apparues dans le typage de expr, trois situations peuvent apparaître : (a) σ et τ sont entièrement déterminés. Le type du paramètre et celui du résultat sont connus. (b) des contraintes contradictoires sont apparues et expr n'est pas typable. L'expression fonctionnelle ne l'est pas non plus. (c) σ et/ou τ ne sont pas entièrement déterminés. La fonction x 7→ expr peut convenir pour plusieurs types de paramètres ou peut avoir plusieurs types de résultat. Illustrons chacune des situations évoquées. Exemple 1 : Considérons la fonction function x -> x+a > 0 et calculons son type dans l'environnement Env =< a : int = 1 > .Env0 où Env0 est l'environnement initial standard de . OCaml 1. soit σ le type (inconnu) de x 2. soit τ le type (inconnu) de x + a > 0 3. dans l'environnment Env , a est de type int, + est de type int -> int -> int, donc x est de type int et σ =int, sous l'hypothèse σ =int, x + a est typable de type int, 0 est de type int, > est polymorphe à valeurs booléennes donc x + a > 0 est typable de type bool, et par conséquent τ =bool. 4. la fonction est donc de type int -> bool. Exemple 2 : Considérons maintenant la fonction function x -> x && (x < 0) ;; dans l'environnement initial de OCaml Env0 . 1. soit σ le type (inconnu) de x 2. soit τ le type (inconnu) de x&&(x < 0) 3. contraintes obtenues dans l'environnement Env dans Env0 , && est de type bool -> bool -> bool donc x doit être de type bool, σ =bool l'expression (x < 0) n'est alors pas typable puisque 0 est de type int diérent du type inféré pour x 4. la fonction n'est pas typable. ce qu'on peut vérier. # function x -> x && (x < 0) ;; Characters 24-25: function x -> x && (x < 0) ;; ^ This expression has type int but is here used with type bool 14 CHAPITRE 2. Exemple 3 : ORDRE SUPÉRIEUR - POLYMORPHISME Considérons enn la fonction function x -> x ;; dans un environnement quelconque Env . 1. soit σ le type (inconnu) de x 2. soit τ le type (inconnu) de x 3. on doit évidemment avoir l'équation σ = τ 4. la liste des contraintes obtenues est limitée l'équation σ = τ , donc le type de la fonction n'est pas complètement déterminé : aucune contrainte sur le type du paramètre, le résultat a le même type que le paramètre. # # - # (function x -> x) 1 ;; : int = 1 (function x -> x) true ;; : bool = true (function x -> x) "elfe" ;; : string = "elfe" 2.2.3 Typage des fonctions récursives Pour le typage d'une déclaration de la forme let rec f = function x -> <expr> dans un environnement Env , il sut de déterminer les contraintes sur les inconnues σ et τ dans l'environnement augmenté Env 0 =< f : σ → τ = x 7→< expr >> .Env 2.2.4 Fonctions polymorphes Les fonctions comme celle de l'exemple 3 sont dites polymorphes car elles peuvent être appelées avec des arguments ayant des types diérents. Pour désigner le type d'un argument d'une fonction polymorphe, trouve le type le plus général et utilise des variables notées ’a, ’b, . . . pour désigner ces types. OCaml # let id x = x ;; val id : ’a -> ’a = <fun> Il existe de nombreuses fonctions ou opérateurs polymorphes en comparaison1 par exemple. # # - 2.3 OCaml, comme les opérateurs de (=) ;; : ’a -> ’a -> bool = <fun> (<) ;; : ’a -> ’a -> bool = <fun> Exercices Exercice 8. ou exclusif Question 1. Réalisez la fonction xor qui calcule le ou-exclusif de ses deux arguments. Déterminez le type de cette fonction. Question 2. Quelles sont les natures des variables dénies par let f = xor true;; let g = xor false;; 1 Ces opérateurs ne s'appliquent toutefois pas sur des fonctions. L'égalité de deux fonctions est une propriété indécidable. 2.3. 15 EXERCICES Exercice 9. Sommes P Question 1. Réalisez une fonction somme1 paramétrée par les entiers a et b qui calcule bk=a k. Question 2. Quel est le type de l'expression somme1 a si a est un nombre entier ? Question 3. Réalisez une P fonction somme paramétrées par une fonction f de type int -> int et deux b entiers a et b qui calcule k=a f (k). Quel est le type de cette fonction ? Question 4. Comment utiliser la fonction somme pour dénir la fonction somme2 qui calcule la somme des carrés des entiers de a à b, a et b étant passés en paramètres ? Exercice 10. Expressions fonctionnelles Donner des expressions fonctionnelles ayant les types suivants : 1. int -> int -> int 2. (int -> int) -> int 3. int -> (int -> int) 4. int -> int -> int -> int 5. int -> (int -> int) -> int 6. (int -> int) -> int -> int 7. (int -> int -> int) -> int Exercice 11. Expressions fonctionnelles polymorphes Donner des expressions fonctionnelles ayant les types suivants : 1. ’a -> ’a 2. ’a -> int 3. ’a -> ’b 4. ’a -> ’a -> bool 5. (’a -> ’b) -> ’a -> ’b 6. (’a -> ’b) -> (’b -> ’c) -> ’a -> ’c 7. (’a -> ’b) -> (’c -> ’b) -> ’a -> ’c -> bool 8. (’a -> int -> int) -> ’a -> ’a -> int 9. (’a -> ’b -> ’c) -> (’a -> ’b) -> ’a -> ’c Exercice 12. Exercice 13. Typage d'expressions fonctionnelles polymorphes Expliquez pourquoi le type des expressions fonctionnelles qui suivent ne dépend pas de l'environnement dans lequel elles sont évaluées, et déterminez ce type. 1. fun f x y -> f x y 2. fun f x y -> (f x) y 3. fun f x y -> f (x y) 4. fun f x y -> f (x y f) 5. fun f x y -> (f x) (y f) 6. fun f x y -> (f x) / (f y) 7. fun f x y -> x f (f y) 8. fun f x y -> f (f x y) Exercice 14. Fonctionnelles polymorphes Réalisez des fonctions d'ordre supérieur polymorphes pour 1. l'application d'une fonction f à un argument x 2. la composition de deux fonctions f et g 3. l'itération n fois d'une fonction f sur un argument x, ie calcul de f n (x) = f (f (. . . f (x) . . .)) 16 CHAPITRE 2. ORDRE SUPÉRIEUR - POLYMORPHISME Chapitre 3 Couples, 3.1 Couples, n-uplets et listes n-uplets 3.1.1 Construction de n-uplets : Les n-uplets sont obtenus avec la virgule : # 1, "a", true ;; - : int * string * bool = (1, "a", true) Le type des expressions ainsi construites est composé des types des diérentes composantes du n-uplet. Comme on le voit sur l'exemple, il est possible de construire des n-uplets dont les composantes sont de types diérents. Un type composé n-uplet correspond à un produit cartésien d'ensembles. Remarque : Toutefois, si le produit cartésien d'ensembles est associatif, il n'en est pas de même de la construction des n-uplets : les types σ1 *σ2 *σ3 , (σ1 *σ2 )*σ3 et σ1 *(σ2 *σ3 ) ne sont pas égaux. # let a1 = 1,"a",true ;; val a1 : int * string * bool = (1, "a", true) # let a2 = (1,"a"),true ;; val a2 : (int * string) * bool = ((1, "a"), true) # let a3 = 1,("a",true) ;; val a3 : int * (string * bool) = (1, ("a", true)) # a1=a2;; Characters 3-5: a1=a2;; ^^ This expression has type (int * string) * bool but is here used with type int * string * bool # a2=a3;; Characters 3-5: a2=a3;; ^^ This expression has type int * (string * bool) but is here used with type (int * string) * bool Le type de a1 est un triplet tandis que les types de a2 et a3 sont des couples de types diérents. 3.1.2 Accès aux composantes d'un n-uplet : On peut accéder aux composantes d'un n-uplet à l'aide d'une déclaration # let a11,a12,a13 = a1;; val a11 : int = 1 val a12 : string = "a" val a13 : bool = true 17 18 CHAPITRE 3. COUPLES, N -UPLETS ET LISTES Cette déclaration associe à trois variables les composantes du triplet a1. Pour n = 2, il existe deux focntions prédénies donnant accès aux composantes du couple : 1. fst : ’a * ’b -> ’a donne la première (rst) composante, 2. snd : ’a * ’b -> ’b donne la première (second) composante. Ces deux fonctions ne peuvent pas s'appliquer sur des n-uplets à plus de deux composantes. # fst a2 ;; - : int * string = (1, "a") # snd a2 ;; - : bool = true # fst a1 ;; Characters 4-6: fst a1 ;; ^^ This expression has type int * string * bool but is here used with type ’a * ’b 3.1.3 Retour sur l'arité des fonctions En mathématiques, une fonction dénie sur un ensemble A × B peut être indiéremment considérée comme une fonction à deux variables x ∈ A et y ∈ B (arité 2), ou bien comme une fonction à une variable c ∈ A × B (arité 1). En informatique, les deux points de vue sont diérents. # let add x y = x+y ;; val add : int -> int -> int = <fun> # let plus (x,y) = x+y ;; val plus : int * int -> int = <fun> # add 1 2 ;; - : int = 3 # plus (1,2) ;; - : int = 3 # add (1,2) ;; Characters 4-9: add (1,2) ;; ^^^^^ This expression has type int * int but is here used with type int # plus 1 2 ;; Characters 0-4: plus 1 2 ;; ^^^^ This function is applied to too many arguments, maybe you forgot a ‘;’ Les deux fonctions add et plus sont diérentes bien que mathématiquement semblables. On dit que curryée 1 de plus, et inversement, plus est la forme décurryée de add. Entre forme curryée ou décurryée d'une même fonction, laquelle le programmeur doit-il choisir ? Cela dépend de l'usage qui en est fait. Cependant il est toujours possible de transformer une fonction d'une forme à l'autre. Un argument en faveur de la forme curryée : la possibilité d'appliquer partiellement la fonction. add est la forme 3.2 Listes 3.2.1 Construction de listes OCaml permet de manipuler des listes homogènes, c'est-à-dire constituée d'éléments du même type. Pour construire des listes on utilise deux constructeurs : [] pour la liste vide, et : : pour l'adjonction d'un élément en tête de liste. 1 Rien à voir avec un quelconque mélange d'épices. Du nom du logicien Haskell Curry (1900-1982) 3.2. 19 LISTES # 1 :: 2 :: 3 :: [];; - : int list = [1; 2; 3] Il est aussi possible de construire une liste par énumération de ses éléments sous la forme # [1; 2; 3] ;; - : int list = [1; 2; 3] Remarque : La construction d'une liste par énumération de ses éléments utilise le séparateur ; à ne pas confondre avec le séparateur ,. # [1,2,3] ;; - : (int * int * int) list = [(1, 2, 3)] 3.2.2 Le type list À la diérence des n-uplets, les listes de OCaml sont homogènes. # [1; "a"; true] ;; Characters 4-7: [1; "a"; true] ;; ^^^ This expression has type string but is here used with type int OCaml précise le type d'une liste en indiquant σ # # # - list où σ est le type des éléments de la liste. [1 ; 2; 3] ;; : int list = [1; 2; 3] [true; false] ;; : bool list = [true; false] [(1,2) ; (3,4)];; : (int * int) list = [(1, 2); (3, 4)] La liste vide peut être d'un type polymorphe si elle est construite avec le constructeur [] # [] ;; - : ’a list = [] ou d'un type instancié si elle est obtenue par à partir d'une autre liste # # # - List.tl [true] ;; : bool list = [] List.tl [1] ;; : int list = [] List.tl ["a"] ;; : string list = [] 3.2.3 Fonctions du module List Les principales opérations sur les listes sont regroupées dans le module List. Pour les utiliser, il faut préxer leur nom par le nom du module List (à moins de faire appel préalablement à la commande open List ; ;. Voici une liste non exhaustive des fonctions du module List. hd : ’a list -> ’a donne le premier élément de la liste. CU : la liste ne doit pas être vide. # List.hd [1;2;3;4] ;; - : int = 1 # List.hd [] ;; Exception: Failure "hd". tl : ’a list -> ’a list donne la liste sans son premier élément. vide. CU : la liste ne doit pas être 20 CHAPITRE 3. COUPLES, N -UPLETS ET LISTES # List.tl [1;2;3;4] ;; - : int list = [2; 3; 4] # List.tl [] ;; Exception: Failure "tl". length : ’a list -> int donne la longueur de la liste. # # - List.length [1;2;3;4] ;; : int = 4 List.length [] ;; : int = 0 nth : ’a list -> int -> ’a donne l'élément d'indice n de la liste, le premier élément étant d'indice 0. CU : n doit être inférieur à la longueur de la liste. # List.nth [1;2;3;4] 0 ;; - : int = 1 # List.nth [1;2;3;4] 3 ;; - : int = 4 # List.nth [1;2;3;4] 4 ;; Exception: Failure "nth". append : ’a list -> ’a list -> ’a list donne la liste concaténée des deux listes. L'opérateur @ peut aussi être utilisé à la place de cette fonction. # # - List.append [1;2;3;4] [5;6] ;; : int list = [1; 2; 3; 4; 5; 6] [1;2;3;4] @ [5;6] ;; : int list = [1; 2; 3; 4; 5; 6] map : (’a -> ’b) -> ’a list -> ’b list applique une fonction à tous les éléments d'une liste pour donner la liste des résultats. # List.map (function x -> x*x) [1;2;3;4] ;; - : int list = [1; 4; 9; 16] 3.3 Types somme 3.3.1 Déclaration de types Parmi les phrases du langage le mot-clé type. OCaml, certaines sont des déclarations de type. Elles commencent par Syntaxe : type <nom> = <def inition> ;; Déclarations simultanées de types type <nom1 > = <def inition1 > and <nom2 > = <def inition2 > ... and <nomn > = <def initionn > 3.3.2 Constucteurs constants On peut dénir des types en utilisant des constructeurs constants qui n'attendent pas d'argument. Les constructeurs sont des noms débutant par une lettre majuscule. # type couleur = Trefle | Pique | Coeur | Carreau ;; type couleur = Trefle | Pique | Coeur | Carreau # Trefle ;; 3.4. 21 FILTRAGE DE MOTIF # # - : couleur = Trefle Pique, Carreau ;; : couleur * couleur = (Pique, Carreau) [Trefle; Coeur; Carreau] ;; : couleur list = [Trefle; Coeur; Carreau] Ada Pascal Les types ainsi dénis sont les analogues de types énumérés de langages comme , , . . . Ils n'ont qu'un nombre ni de valeurs correspondant à chacun des constructeurs utilisés dans l'énumération. 3.3.3 Constructeurs non constants L'usage de constructeurs permet de dénir de nouveaux types que l'on peut considérer comme réunion d'autres types. Le type couleur déni ci-dessus est la réunion de quatre types ayant chacun une valeur. Avec des constructeurs non constants, on peut réunir des types existant pour n'en faire qu'un seul. # type entier_ou_booleen = Entier of int | Booleen of bool ;; type entier_ou_booleen = Entier of int | Booleen of bool # Entier 1 ;; - : entier_ou_booleen = Entier 1 # Booleen true ;; - : entier_ou_booleen = Booleen true Le type entier_ou_booleen ainsi déclaré réunit en un seul type le type bool et le type int. Les constructeurs Entier et Booleen prennent chacun un argument qui est un type placé après le mot-clé of. 3.3.4 Types récursifs On peut faire des déclarations récursives de types, par exemple pour dénir des types listes, arbres. . . L'uage du seul mot-clé type sut à la déclaration de types récursifs. Voici par exemple la déclaration d'un type liste d'entiers # type liste_d_entiers = Nil | Cons of int*liste_d_entiers ;; type liste_d_entiers = Nil | Cons of int * liste_d_entiers # Nil ;; - : liste_d_entiers = Vide # Cons (1, Nil) ;; - : liste_d_entiers = Cons (1, Nil) # Cons(2,Cons (1, Vide)) ;; - : liste_d_entiers = Cons (2, Cons (1, Nil)) et celui d'un type arbre binaire dont les noeuds contiennent des entiers # type ’a arbre_binaire = Vide | Noeud of ’a * ’a arbre_binaire * ’a arbre_binaire ;; type ’a arbre_binaire = Vide | Noeud of ’a * ’a arbre_binaire * ’a arbre_binaire # Vide ;; - : ’a arbre_binaire = Vide # Noeud (3, Vide, Vide) ;; - : int arbre_binaire = Noeud (3, Vide, Vide) # Noeud ("b", Vide, Noeud ("a", Vide, Vide)) ;; - : string arbre_binaire = Noeud ("3", Vide, Noeud ("1", Vide, Vide)) 3.4 Filtrage de motif Le ltrage de motif (pattern-matching en anglais) est une remarquable et puissante caractéristique des langages de la famille ML. 22 CHAPITRE 3. 3.4.1 L'expression match Syntaxe de COUPLES, N -UPLETS ET LISTES ... with match match expr with | m1 -> expr1 | m2 -> expr2 ... | mn -> exprn Dans une expression match l'expression expr est ltrée séquentiellement par les motifs mi . La valeur de cette expression est celle de l'expression expri correpsondant au premier motif mi cohérent avec expr. Exemples 1. Conversion d'une couleur en string # let string_of_couleur c = match c with | Carreau -> "carreau" | Coeur -> "coeur" | Pique -> "pique" | Trefle -> "trèfle" ;; val string_of_couleur : couleur -> string = <fun> # List.map string_of_couleur [Trefle; Carreau; Pique ; Coeur] ;; - : string list = ["trèfle"; "carreau"; "pique"; "coeur"] 2. Le ou-exclusif # let xor a b = match (a,b) with | (true,true) -> false | (true,false) -> true | (false,true) -> true | (false,false) -> false ;; val xor : bool -> bool -> bool = <fun> Nécessité d'un ltrage exhaustif Un ltrage doit être exhaustif, c'est-à-dire envisager tous les cas. S'il ne l'est pas, l'interprète s'empresse de le signaler par un avertissement. # let xor a b = match (a,b) with | (true,true) -> false | (true,false) -> true | (false,true) -> true ;; Characters 15-102: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: (false, false) match (a,b) with | (true,true) -> false | (true,false) -> true | (false,true) -> true... val xor : bool -> bool -> bool = <fun> Motif universel Supposons que nous voulions écrire une fonction chiffre : char -> bool qui teste si le caractère passé en paramètre est un chire. Il serait pénible d'écrire cette fonction par ltrage de motif en examinant chacun des 256 caractères. Il est alors judicieux d'utiliser le motif universel _ satisfait par toutes les données. 3.4. FILTRAGE DE MOTIF 23 # let chiffre c = match c with ’0’ | ’1’ | ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ | ’8’ | ’9’ -> true | _ -> false ;; val chiffre : char -> bool = <fun> # List.map chiffre [’0’ ; ’A’ ; ’8’] ;; - : bool list = [true; false; true] Motif avec variables Un motif peut contenir une variable. Voici une dénition plus concise du ou-exclusif utilisant des motifs avec variable. # let xor a b = match (a,b) with | (true,b) -> not b | (_,b) -> b ;; val xor : bool -> bool -> bool = <fun> Toutefois, une variable ne peut avoir qu'une seule occurrence dans un motif. Il pourrait être tentant d'écrire l'égalité en utilisant le ltrage # let egal x y = match (x,y) with | (x,x) -> true | _ -> false ;; Characters 38-39: | (x,x) -> true ^ This variable is bound several times in this matching mais il est refusé par l'interprète. 3.4.2 Filtrage de paramètres On trouve des motifs partout : les déclarations de variables suivent le schéma let motif =expr, comme par exemple dans # let a,b = 1,2 ;; val a : int = 1 val b : int = 2 la syntaxe des expressions function suit le schéma function motif -> expr, comme par exemple dans # let fst = function (x,y) -> x ;; val fst : ’a * ’b -> ’a = <fun> ou encore dans # let rec fact = function 0 -> 1 | n -> n*fact(n-1) ;; val fact : int -> int = <fun> C'est surtout dans la dénition de fonctions à paramètres de type que le ltrage trouve son application. Exemples 1. ltrage de liste # let rec miroir = function | [] -> [] | x::l -> miroir(l)@[x] ;; val miroir : ’a list -> ’a list = <fun> # miroir [1;2;3] ;; - : int list = [3; 2; 1] 24 CHAPITRE 3. COUPLES, N -UPLETS ET LISTES 2. ltrage de type somme # let rec longueur = function | Nil -> 0 | Cons (_,l) -> 1 + longueur l ;; val longueur : liste_d_entiers -> int = <fun> # longueur (Cons(1,Cons(2,Nil))) ;; - : int = 2 3.5 Exercices Exercice 15. Fonctions fst et snd Écrivez les fonctions prédénies fst et snd. Exercice 16. Accès aux composantes d'un n-uplet Question 1. Réalisez une fonction qui donne la valeur de la deuxième composante d'un triplet. Question 2. Réalisez une fonction qui donne la valeur de la i-ème composante d'un triplet, 1 ≤ i ≤ 3 étant passé en paramètre. Quelle contrainte cela impose-t-il sur le triplet ? Exercice 17. Currycation décurrycation Question 1. Quelle diérence y a-t-il entre les deux fonctions let add x y = x+y ;; let plus (x,y) = x+y ;; En mathématiques, une fonction dénie sur N2 peut être indiéremment considérée comme une fonction à deux variables dans N (comme la fonction add), ou comme une fonction à une variable dans N2 (comme la fonction plus). En informatique (et en logique), les deux fonctions sont deux formes diérentes de la même fonction : Question 2. Déterminez le type et réalisez la fonction curryfie (resp. decurryfie) qui transforme une fonction à un argument couple (resp. à deux arguments) en sa forme curryée (resp. décurryée). Exercice 18. Listes et premières manipulations Pour la session qui suit, donnez le type et la valeur de chacune des phrases let let let let let let lvide = [] ;; lpos = [1; 2; 3] ;; lpos = 19::lpos ;; lpos = 13::42::lpos;; lneg = [(-1) ; (-2) ; (-3)] ;; lneg = [(-7) ; (-8) ; (-9)] @ lpos ;; List.hd List.tl List.length List.nth List.rev List.mem 2 lpos lpos lpos lpos lpos lpos ;; (* donne la tete de la liste *) ;; (* donne la liste sans sa tete *) ;; 5 ;; ;; ;; (* test d’appartenance de l’element 2 *) let lfrag = [1 ; "chaine" ; true ];; List.hd lvide ;; -> Uncaught exception Failure("hd") Exercice 19. Typage de listes Donnez le type des listes 1. [] 2. [[]] 3. [[] ;[[]]] 4. [[],[[]]] 3.5. 25 EXERCICES Exercice 20. Listes et fonctions de manipulation avancées Pour la session qui suit, donnez le type et la valeur de chacune des phrases let lpos = [1;8;9;5;7;3;2];; (* * List.find : (’a -> bool) -> a’ list -> a’ = <fun> * recherche du premier element verifiant le predicat *) ’a -> bool List.find (function x -> x mod 2 = 0) lpos;; (* * List.find_all ou List.filter : (’a -> bool) -> a’ list -> a’ list = <fun> * recherche de tous les elements verifiant le predicat ’a -> bool * résultat de type ’a list *) List.find_all (function x -> x mod 2 = 0) lpos;; List.filter (function x -> not (x mod 2 = 0)) lpos;; (* * List.partition :(’a -> bool) -> a’ list -> ’a list * ’a list * partition des elements selon un predicat ’a -> bool * type résultat ’a list * ’a list *) List.partition (function x -> x mod 2 = 0) lpos;; (* List.iter : (’a -> unit) -> ’a list -> unit * applique la fonction ’a -> unit a chacun des elements *) List.iter (function x -> print_string "(" ; print_int x ; print_string ")") lpos;; (* List.map : (’a -> ’b) -> a’ list -> b’ list * applique la fonction ’a -> ’b a chacun des elements de la liste * résultat de type ’b list *) List.map (function x -> x * x) lpos;; (* List.flatten : ’a list list -> a’ list * applatissement des a’ listes de listes en a’ listes *) List.flatten [[1;2;3];[4;5];[];[6;7]];; List.flatten [[[1];[2;3]];[[4];[5]];[[]];[[6];[7]]];; Exercice 21. Listes et redénition des fonctions de manipulation Question 1. Réécrire la fonction length d'un liste en utilisant la fonction List.tl. Question 2. Réécrire la fonction nth en utilisant les fonctions List.tl et List.hd . Question 3. Réécrire la fonction rev à l'aide de la fonction de concatenation @ . Question 4. Réécrire la fonction map en utilisant les fonctions List.tl et List.hd. Question 5. Réécrire la fonction filter à l'aide des fonctions List.tl et List.hd. Question 6. Dénissez une procédure qui ache une expression arithmétique sous forme inxée complètement parenthésée. 26 CHAPITRE 3. COUPLES, N -UPLETS ET LISTES Chapitre 4 Peut-on tout programmer avec des fonctions ? Le but de ces quelques notes est de montrer qu'il est possible de représenter les nombres entiers, les booléens et les couples par des fonctions pures, et de se convaincre que tout ce qui est programmable peut l'être avec le seul formalisme des fonctions. La base théorique sous-jacente est le λ-calcul introduit en 1932 par Alonzo Church pour tenter de cerner la notion de calculabilité. Tout est exprimé dans le langage , en s'interdisant le recours aux nombres, booléens et n-uplets de ce langage, ainsi qu'aux formes if et let rec, et les opérateurs ou fonctions prédénies. Les seuls éléments du langage que nous nous autoriserons sont les identicateurs de variables, l'application d'une fonction à un argument et la construction d'une fonction (forme function x -> ...). Par commodité, nous nous autoriserons aussi la forme let pour nommer certains termes. Caml 4.1 Les entiers 4.1.1 Représentation de Church Observons la représentation des quatre premiers nombres entiers 0, 1, 2 et 3 donnée par les dénitions suivantes : (* l’entier zéro *) let zero = fun f x -> x ;; (* l’entier un *) let un = fun f x -> f x ;; (* l’entier deux *) let deux = fun f x -> f (f x) ;; (* l’entier trois *) let trois = fun f x -> f (f (f x)) ;; Chacune de ces dénitions indique que l'entier n est représenté comme une fonction qui itère une certaine fonction f n fois depuis une donnée x. Plus généralement, la représentation de Church d'un entier n ∈ N peut être donnée par l'expression fun f x -> f (f ... (f x) ;; dans laquelle f apparaît n fois. Application : Cette représentation d'un entier n peut être utilisée pour itérer n fois une fonction f depuis un élément x. Par exemple, en prenant pour fonction f la fonction prédénie succ et pour élément x l'entier (de ) 0, on a Caml # zero succ 0 ;; - : int = 0 27 28 CHAPITRE 4. # # # - PEUT-ON TOUT PROGRAMMER AVEC DES FONCTIONS ? un succ 0 ;; : int = 1 deux succ 0 ;; : int = 2 trois succ 0 ;; : int = 3 Cela suggère la fonction de conversion d'un entier représenté sous forme de Church en un entier de Caml. let entier2int n = n succ 0 ;; Remarque : le type général de la représentation de Church d'un entier est (’a -> ’a) -> ’a -> ’a. Toutefois, le type de zero est ’a -> ’b -> ’b et celui de un est (’a -> ’b) -> ’a -> ’b. Le type (’a -> ’a) -> ’a -> ’a est l'unicateur le plus général de ces deux types. 4.1.2 La fonction successeur La fonction successeur doit transformer un entier n en l'entier n+1. Lorsque les entiers sont représentés sous forme de Church, cela revient à ajouter une application supplémentaire de la fonction intervenant dans la représentation. Cet ajout peut se faire de deux manières : 1. on applique la fonction f au résultat de l'itération let suc1 = fun n f x -> f (n f x) ;; 2. on applique la fonction f à l'élément x avant d'appliquer n let suc2 = fun n f x -> n f (f x) ;; # # - List.map entier2int : int list = [1; 2; List.map entier2int : int list = [1; 2; (List.map suc1 [zero; un; deux; trois]) ;; 3; 4] (List.map suc2 [zero; un; deux; trois]) ;; 3; 4] Remarques : 1. Le type de la fonction suc1 est ((’a -> ’b) -> ’c -> ’a) -> (’a -> ’b) -> ’c -> ’b et celui de la fonction suc2 est ((’a -> ’b) -> ’b -> ’c) -> (’a -> ’b) -> ’a -> ’c. Chacun de ces types est bien plus général que ((’a -> ’a) -> ’a -> ’a) -> (’a -> ’a) -> ’a -> ’a. 2. Les entiers donnés par ces deux fonctions ne sont pas de type (’a -> ’a) -> ’a -> ’a mais de type (’_a -> ’_a) -> ’_a -> ’_a. # let quatre1 val quatre1 : # let quatre2 val quatre2 : = suc1 trois (’_a -> ’_a) = suc2 trois (’_a -> ’_a) ;; -> ’_a -> ’_a = <fun> ;; -> ’_a -> ’_a = <fun> La variable de type ’_a indique que le type de quatre1 est en attente d'instanciation. C'est la première utilisation de quatre1 qui xera la valeur de ’_a. # # # - quatre1 ;; : (’_a -> ’_a) -> ’_a -> ’_a = <fun> quatre1 succ ;; : int -> int = <fun> quatre1 ;; : (int -> int) -> int -> int = <fun> 4.1. 29 LES ENTIERS 4.1.3 L'addition Pour dénir la somme de deux entiers n et m, on peut s'y prendre de deux façons : 1. on peut considérer que la somme n + m est obtenue en itérant n fois la fonction successeur sur m let add1 = fun n m -> n suc1 m ;; 2. ou si on préfère ne pas utiliser la fonction successeur, la somme n+m peut être vue comme l'itération n fois d'une fonction f sur un élément obtenu en itérant m fois la même fonction f let add2 = fun n m f x -> n f (m f x) ;; # # # # - List.map entier2int : int list = [0; 1; List.map entier2int : int list = [1; 2; List.map entier2int : int list = [2; 3; List.map entier2int : int list = [3; 4; (List.map 2; 3] (List.map 3; 4] (List.map 4; 5] (List.map 5; 6] (add1 zero) [zero; un; deux; trois]) ;; (add1 un) [zero; un; deux; trois]) ;; (add1 deux) [zero; un; deux; trois]) ;; (add1 trois) [zero; un; deux; trois]) ;; 4.1.4 La multiplication Le produit de deux entiers n et m est l'itération n fois de la fonction f itérée m fois. let mult = fun n m f -> n (m f);; # # # # - List.map entier2int : int list = [0; 0; List.map entier2int : int list = [0; 1; List.map entier2int : int list = [0; 2; List.map entier2int : int list = [0; 3; (List.map 0; 0] (List.map 2; 3] (List.map 4; 6] (List.map 6; 9] (mult zero) [zero; un; deux; trois]) ;; (mult un) [zero; un; deux; trois]) ;; (mult deux) [zero; un; deux; trois]) ;; (mult trois) [zero; un; deux; trois]) ;; 4.1.5 L'exponentiation L'exponentiation mn est encore plus simple à dénir puisqu'on l'obtient en itérant n fois l'entier m. let exp = fun n m -> n m;; # # # # - List.map entier2int : int list = [1; 1; List.map entier2int : int list = [0; 1; List.map entier2int : int list = [0; 1; List.map entier2int : int list = [0; 1; (List.map 1; 1] (List.map 2; 3] (List.map 4; 9] (List.map 8; 27] (exp zero) [zero; un; deux; trois]) ;; (exp un) [zero; un; deux; trois]) ;; (exp deux) [zero; un; deux; trois]) ;; (exp trois) [zero; un; deux; trois]) ;; 4.1.6 Et la soustraction ? La soustraction va nécessiter de dénir les booléens et les couples. 30 CHAPITRE 4. 4.2 PEUT-ON TOUT PROGRAMMER AVEC DES FONCTIONS ? Les booléens 4.2.1 Représentation des deux booléens Nous dénirons les deux valeurs booléennes par (* Le vrai *) let vrai = fun x y -> x ;; (* le faux *) let faux = fun x y -> y ;; Ces dénitions se justieront par l'usage que nous en ferons pour dénir la conditionnelle. La fonction booleen2bool convertit les termes vrai et faux en les booléens de . Caml let booleen2bool b = b true false ;; Remarque : Le booléen faux est représenté par le même terme que l'entier 0. Remarque : le type de vrai est ’a -> ’b -> ’a et celui de faux ’a -> ’b -> ’b. Le type le plus général qui unie ces deux types est ’a -> ’a -> ’a. 4.2.2 La conditionnelle La fonction conditionnelle permet d'exprimer la forme si ... alors ... sinon .... Le choix de la représentation des booléens rend très aisée la dénition de cette fonction. (* la fonction conditionnelle * cond c a s = a si c est vrai = s sinon * ) * let cond = fun c a s -> c a s ;; # # - cond vrai 1 2;; : int = 1 cond faux 1 2;; : int = 2 Remarque : s'il est facile de dénir la fonction conditionnelle, son utilisation pose néanmoins le problème de l'évaluation d'une expression conditionnelle. Si pour évaluer un appel de fonction, on emploie la stratégie d'évaluation préalable de tous ses arguments, il peut arriver certaines dicultés. adopte l'évaluation complète des arguments d'un appel de fonction et on comprend pourquoi l'évaluation de l'expression cond vrai 1 1/0, qui vaut 1, pose problème. OCaml # cond vrai 1 1/0;; Exception: Division_by_zero. Pour éviter ce genre de problème, il est nécessaire d'adopter une autre stratégie nommée évaluation paresseuse qui consiste à n'évaluer les expressions que lorsqu'on y est obligé. 4.2.3 Les opérateurs logiques La fonction cond permet une dénition aisée des opérateurs booléens. (* l’opérateur de négation *) let non = function b -> cond b faux vrai ;; (* la conjonction *) let et = fun b1 b2 -> cond b1 b2 faux ;; (* la disjonction *) let ou = fun b1 b2 -> cond b1 vrai b2 ;; 4.3. 31 LES COUPLES # # # # # - List.map booleen2bool (List.map : bool list = [false; true] List.map booleen2bool (List.map : bool list = [true; false] List.map booleen2bool (List.map : bool list = [false; false] List.map booleen2bool (List.map : bool list = [true; true] List.map booleen2bool (List.map : bool list = [true; false] non [vrai ; faux]) ;; (et vrai) [vrai ; faux]) ;; (et faux) [vrai ; faux]) ;; (ou vrai) [vrai ; faux]) ;;; (ou faux) [vrai ; faux]) ;;; 4.2.4 Un test de nullité On est en mesure maintenant de dénir le test de nullité d'un entier. let estNul = function n -> n (function x -> faux) vrai ;; # List.map booleen2bool (List.map estNul [zero; un; deux; trois]) ;; - : bool list = [true; false; false; false] 4.3 Les couples Par dénition, un couple contient deux choses. On doit pouvoir construire un couple à partir de n'importe quelles deux choses, et extraire l'une ou l'autre des choses contenues dans un couple. Le couple (x, y) peut être représenté par la fonction function z -> z x y. La conversion de ces couples en couples de s'obtient avec la fonction Caml let couple2caml = function c -> (c vrai),(c faux) ;; Remarque : le type le plus général d'un couple est donc (’a -> ’b -> ’c) -> ’c. 4.3.1 Le constructeur Le constructeur de couple est la fonction cons. let cons = fun x y -> function z -> z x y ;; # # - cons 1 2;; : (int -> int -> ’_a) -> ’_a = <fun> couple2caml (cons 1 2) ;; : int * int = (1, 2) Remarque : le champ de notre étude est indépendant de la notion de type (c'est du λ-calcul pur). Cependant, l'utilisation de amène quelques restrictions d'usage de la fonction couple2caml. OCaml # cons 1 true ;; - : (int -> bool -> ’_a) -> ’_a = <fun> # couple2caml (cons 1 true) ;; Characters 12-25: couple2caml (cons 1 true) ;; ^^^^^^^^^^^^^ This expression has type (int -> bool -> ’a) -> ’a but is here used with type (int -> int -> int) -> ’b 32 CHAPITRE 4. PEUT-ON TOUT PROGRAMMER AVEC DES FONCTIONS ? 4.3.2 Les sélecteurs Les sélecteurs des composantes d'un couple s'obtiennent aisément avec les booléens. (* sélection de la première composante *) let car = function c -> c vrai ;; (* sélection de la seconde composante *) let cdr = function c -> c faux ;; # # - entier2int (car (cons un deux)) ;; : int = 1 entier2int (cdr (cons un deux)) ;; : int = 2 4.3.3 Prédécesseur d'un entier Avec les couples il est possible de calculer le prédécesseur d'un entier n. Il sut d'itérer n fois la fonction qui à un couple (a, b) associe le couple (b, b + 1) à partir du couple (0, 0). (* prédécesseur * pred n = entier qui précède n *) let pred = function n -> car (n (function c -> cons (cdr c) (suc1 (cdr c))) (cons zero zero));; # List.map entier2int (List.map pred [zero; un; deux; trois]) ;; - : int list = [0; 0; 1; 2] Remarque : Avec cette dénition, zero est son propre prédécesseur. 4.3.4 Soustraction La soustraction peut se dénir par itération du prédécesseur. let sub = fun n m -> m pred n ;; 4.3.5 Test d'égalité Une idée naturelle pour décrire la fonction d'égalité de deux entiers est de tester la nullité de la diérence de ces deux entiers. Mais cette idée ne peut s'appliquer en utilisant la fonction sub car elle attribue la valeur zero à n − m lorsque n ≤ m. let inf = fun n m -> estNul (sub n m) ;; let egal = fun n m -> et (inf n m) (inf m n) ;; 4.4 La récursivité 4.4.1 Factorielle avec des couples On peut calculer n! en itérant n fois la fonction (a, b) 7→ (a + 1, (a + 1)b) depuis le couple (0, 1). let fact = function n -> cdr (n (function c -> (cons (suc1 (car c)) (mult (suc1 (car c)) (cdr c) ))) (cons zero un)) ;; # # - List.map entier2int (List.map fact [zero; un; deux; trois]) ;; : int list = [1; 1; 2; 6] entier2int (fact (exp deux trois)) ;; : int = 362880 4.4. 33 LA RÉCURSIVITÉ 4.4.2 Point xe On imagine bien pouvoir écrire, de la même façon que la fonction fact ci-dessus, toute fonction que l'on programmerait autrement à l'aide d'une boucle pour1 . Mais qu'en estil pour les fonctions qui n'entrent pas dans ce cadre2 ? Nous allons proposer une autre solution pour la fonction factorielle qui suit un schéma général applicable à toute fonction. Cette approche est celle du calcul du plus petit point xe d'une fonctionnelle, point xe qui peut être déterminé par un opérateur nommé combinateur de point xe. L'ensemble ordonné F Notons F = NN l'ensemble de toutes les fonctions de N dans N. Les fonctions considérées ici ne sont pas nécessairement dénies sur N tout entier, c'estàdire qu'étant donné f ∈ F , il peut exister des valeurs de n ∈ N telles que f (n) ne soit pas déni. Nous noterons ⊥ la (seule) fonction de F dénie nulle part. 3 la fonction ⊥ : Voici écrite en Caml let bottom = function x -> (function y -> y y) (function y -> y y);; Toute tentative d'application de la fonction bottom à un quelconque argument conduit à une évaluation innie. # bottom 1 ;; Interrupted. On dira qu'une fonction f ∈ F est moins dénie qu'une fonction g ∈ F , et on notera f 4 g , si pour tout entier n où la fonction f est dénie, on a f (n) = g(n), autrement dit si f et g coincident sur l'ensemble de dénition de f . La relation 4 sur l'ensemble F en fait un ensemble non totalement ordonné admettant la fonction ⊥ comme plus petit élément, et les fonctions partout dénies comme éléments maximaux. Fonctionnelles Nous appellerons fonctionnelle toute application de F dans lui-même. Par exemple, la fonctionnelle Φfact dénie par Φfact : F −→ F f 7−→ g = Φfact (f ) où g est la fonction dénie par g(n) = 1 si n = 0 . n × f (n − 1) sinon Appliquons Φfact à la fonction ⊥ f1 = Φfact (⊥). Par la dénition même de Φfact , la fonction f1 est dénie en 0 et vaut 1. Et elle n'est dénie nulle part ailleurs, étant donné que ⊥ n'est dénie nulle part. Appliquons maintenant Φfact à f1 f2 = Φfact (f1 ). Cette fonction f2 est dénie en 0 où elle prend la même valeur que f1 , et en 1 où elle vaut 1. Ainsi f1 est moins dénie que f2 . D'autre part, f2 n'est dénie pour aucun entier supérieur à 1. Si on considère la suite de fonctions (fn )n∈N de F dénie par récurrence par f0 = ⊥ fn+1 = Φfact (fn ) on obtient des fonctions telles que 1 Dans la terminomogie des fonctions récursives, ce sont les fonctions primitives récursives 2 Comme par exemple, le calcul du pgcd de deux entiers, ou encore le calcul de la longueur d'une liste. 3 Par défaut, refuse la dénition de bottom car l'expression y y n'est pas typable. Il faut autoriser OCaml les types récursifs (option -rectypes de la commande ocaml) pour que Caml accepte cette dénition en lui donnant alors le type ’a -> ’b. 34 CHAPITRE 4. PEUT-ON TOUT PROGRAMMER AVEC DES FONCTIONS ? 1. fn (k) = k! si k < n ; 2. et fn (k) non déni si k ≥ n. On peut remarquer que cette suite est constituée de fonctions de mieux en mieux dénies f0 4 f1 4 f2 4 . . . 4 fn 4 . . . autrement dit la suite est croissante dans F , et que la fonction factorielle (fact) est mieux dénie que toutes les fonctions de cette suite, ∀n ∈ N fn 4 fact. On peut dénir tout cela en Caml4 par let phiFact = fun f n -> if n=0 then 1 else n * f (n - 1) ;; let f0 = bottom ;; let f1 = phiFact f0 ;; let f2 = phiFact f1 ;; let f3 = phiFact f2 ;; let f4 = phiFact f3 ;; et on peut vérier que les fonctions ainsi dénies sont des fonctions qui approchent de mieux en mieux la fonction factorielle. # List.map f2 [0;1] ;; - : int list = [1; 1] # List.map f3 [0;1;2] ;; - : int list = [1; 1; 2] # List.map f4 [0;1;2;3] ;; - : int list = [1; 1; 2; 6] # f4 4 ;; Interrupted. Point xe Quelle est la fonction obtenue si on applique la fonctionnelle Φfact à la fonction fact ? On peut vérier que l'on a Φfact (fact) = fact. Trichons un peu et vérions cette égalité : # let rec fact n = if n=0 then 1 else n*fact(n-1) ;; val fact : int -> int = <fun> # List.map fact [1;2;3;4;5;6;7;8;9;10] ;; - : int list = [1; 2; 6; 24; 120; 720; 5040; 40320; 362880; 3628800] # let fact2 = phiFact fact ;; val fact2 : int -> int = <fun> # List.map fact2 [1;2;3;4;5;6;7;8;9;10] ;; - : int list = [1; 2; 6; 24; 120; 720; 5040; 40320; 362880; 3628800] Autrement dit, la fonction fact est un point xe de la fonctionnelle Φf act . Une fonction f ∈ F est un point xe d'une fonctionnelle Φ si on a l'égalité Φ(f ) = f. 4 Nous abandonnons les contraintes xées au début de ce document an de permettre une meilleure lecture du code et faciliter la compréhension. 4.5. 35 EXERCICES Remarque : Deux questions naturelles se posent 1. est-ce que toute fonctionnelle admet des points xes ? 2. y a-t-il unicité du point xe ? La réponse à ces deux questions est négative en général. L'existence est assurée si la fonctionnelle satisfait une propriété de monotonie, c'estàdire si Φ(f ) 4 Φ(g) dès lors que f 4 g 5 . Φfact est une fonctionnelle monotone, et la fonction fact est donc son (plus petit) point xe. Combinateur de point xe Existe-t-il un moyen de calculer le point xe d'une fonctionnelle (s'il existe). La réponse est armative, et il existe plusieurs opérateurs (ou fonctions ou algorithmes) permettant de les calculer. On les appelle combinateurs de point xe. En voici un, nommé combinateur de Curry, dont nous donnons la dénition en . Caml let fixCurry = function f -> (function x -> f (x x)) (function x -> f (x x)) ;; Vérions que fixCurry est un combinateur de point xe. Soit Φ une fonctionnelle. fixCurry Φ = → → = (function f -> (function x -> f (x x)) (function x -> f (x x))) Φ (function x -> Φ (x x)) (function x -> Φ (x x)) Φ ((function x -> Φ (x x)) (function x -> Φ (x x))) Φ (fixcurry Φ). La èche → indique une étape de réduction. Ainsi, le terme fixCurry Φ est un point xe de Φ. Modions la dénition précédente de fixCurry pour le rendre opérationnel en Caml. let fixCurry = function f -> (fun x y -> f (x x) y) (fun x y -> f (x x) y) ;; Appliquons ce combinateur à Φfact let fact = fixCurry phiFact ;; et vérions le point xe obtenu # List.map fact [1;2;3;4;5;6;7;8;9;10] ;; - : int list = [1; 2; 6; 24; 120; 720; 5040; 40320; 362880; 3628800] Exercice 22. Dénir de façon analogue les fonctions 1. bonacci 2. pgcd de deux entiers 3. longueur d'une liste 4. concaténation de deux listes. 4.5 5 pour Exercices en savoir plus à ce sujet cf A COMPLETER 36 CHAPITRE 4. PEUT-ON TOUT PROGRAMMER AVEC DES FONCTIONS ? Bibliographie [Cha00] Emmanuel Chailloux, Pascal Manoury et Bruno Pagano Développement d'applications avec Objective Caml O'Reilly, 2000 [Narb05] Philippe Narbel Programmation fonctionnelle, générique et objet Vuibert, 2005 37 38 BIBLIOGRAPHIE Table des matières 1 Programmation fonctionnelle 1.1 1.2 1.3 Programmation fonctionnelle . . . . . 1.1.1 . . . . . . . . . . . . . . Premiers pas . . . . . . . . . . . . . . 1.2.1 Phrases du langage . . . . . . . 1.2.2 Types de base . . . . . . . . . . 1.2.3 Déclaration de variables . . . . 1.2.4 Déclaration locale de variables 1.2.5 Expression conditionnelle . . . 1.2.6 Fonctions . . . . . . . . . . . . 1.2.7 Déclarations récursives . . . . . Exercices . . . . . . . . . . . . . . . . Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Ordre supérieur - Polymorphisme 2.1 2.2 2.3 Ordre supérieur . . . . . . . . . . . . . . . . . 2.1.1 Les fonctions d'arité supérieure à 1 . . 2.1.2 Fonctionnelles . . . . . . . . . . . . . . Polymorphisme . . . . . . . . . . . . . . . . . 2.2.1 Fonctions polymorphes . . . . . . . . . 2.2.2 Typage d'une expression fonctionnelle 2.2.3 Typage des fonctions récursives . . . . 2.2.4 Fonctions polymorphes . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . 3 Couples, n-uplets et listes 3.1 3.2 3.3 3.4 3.5 Couples, n-uplets . . . . . . . . . . . . . . . . 3.1.1 Construction de n-uplets : . . . . . . . 3.1.2 Accès aux composantes d'un n-uplet : 3.1.3 Retour sur l'arité des fonctions . . . . Listes . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Construction de listes . . . . . . . . . 3.2.2 Le type list . . . . . . . . . . . . . . 3.2.3 Fonctions du module List . . . . . . . Types somme . . . . . . . . . . . . . . . . . . 3.3.1 Déclaration de types . . . . . . . . . . 3.3.2 Constucteurs constants . . . . . . . . 3.3.3 Constructeurs non constants . . . . . 3.3.4 Types récursifs . . . . . . . . . . . . . Filtrage de motif . . . . . . . . . . . . . . . . 3.4.1 L'expression match ... with . . . . . 3.4.2 Filtrage de paramètres . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . 39 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 4 4 4 6 7 7 8 9 9 11 11 11 12 12 12 13 14 14 14 17 17 17 17 18 18 18 19 19 20 20 20 21 21 21 22 23 24 40 TABLE DES MATIÈRES 4 Peut-on tout programmer avec des fonctions ? 4.1 4.2 4.3 4.4 4.5 Les entiers . . . . . . . . . . . . . . . . . . 4.1.1 Représentation de Church . . . . . 4.1.2 La fonction successeur . . . . . . . 4.1.3 L'addition . . . . . . . . . . . . . . 4.1.4 La multiplication . . . . . . . . . . 4.1.5 L'exponentiation . . . . . . . . . . 4.1.6 Et la soustraction ? . . . . . . . . . Les booléens . . . . . . . . . . . . . . . . . 4.2.1 Représentation des deux booléens . 4.2.2 La conditionnelle . . . . . . . . . . 4.2.3 Les opérateurs logiques . . . . . . 4.2.4 Un test de nullité . . . . . . . . . . Les couples . . . . . . . . . . . . . . . . . 4.3.1 Le constructeur . . . . . . . . . . . 4.3.2 Les sélecteurs . . . . . . . . . . . . 4.3.3 Prédécesseur d'un entier . . . . . . 4.3.4 Soustraction . . . . . . . . . . . . 4.3.5 Test d'égalité . . . . . . . . . . . . La récursivité . . . . . . . . . . . . . . . . 4.4.1 Factorielle avec des couples . . . . 4.4.2 Point xe . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 27 28 29 29 29 29 30 30 30 30 31 31 31 32 32 32 32 32 32 33 35