Lycée Thiers Année 2017-18 Sup MPSI Option Informatique OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS En utilisant la construction 1. Filtrage par motifs match expr with m 1 expr 1 m n expr n on peut confronter la valeur d une expression à une succession de motifs, jusqu à ce qu une correspondance soit trouvée. C est le filtrage par motifs (pattern matching en anglais) : il sert à reconnaître la forme de cette valeur et permet d orienter le calcul en associant à chaque motif une expression à évaluer (il est nécessaire que toutes les expr i soient de même type, qui est aussi le type de toute l expression encadrée). Si la valeur de l expression expr est filtrée par le premier motif (càd : si elle lui correspond), alors la valeur de l expression complète (càd : celle qui est encadrée ci-dessus) sera celle de expr 1. Sinon, le motif suivant est essayé et ainsi de suite : le premier motif qui convient (c est-à-dire, qui est plus général que la valeur filtrée) est sélectionné. OCaml est capable de détecter que les cas envisagés ne couvrent pas l ensemble de toutes les éventualités; un warning (mise en garde...) est alors emis ( Warning : this pattern-matching is not exhaustive. ) à la compilation. Si la valeur n est filtrée par aucun motif, une erreur se produit (Match Failure) à l exécution. Pour que ceci n arrive pas, on peut utiliser le joker _, motif universel, qui signifie et pour tout autre motif. Examinons à présent quelques exemples : 1.1. Une fonction définie cas par cas. let f n = if n = 0 then 1 else if n = 1 or n = 2 then 5 else if n = 3 then 10 else 20 est sans doute plus pénible à écrire (et à lire!) que : let f n = match n with 0 -> 1 1 2 -> 5 3 -> 10 _ -> 20 qui peut à son tour s écrire plus simplement (noter que l argument de f n est plus explicité! Il est filtré ) : let f = function 0 -> 1 1 2 -> 5 3 -> 10 _ -> 20
2 OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 1.2. La fonction factorielle. On a déjà eu l occasion de la définir ainsi : let rec fact n = if n = 0 then 1 else n * fact (n-1) val fact : int -> int = <fun> Voici une version avec filtrage : let rec fact n = match n with 0 -> 1 _ -> n * fact (n-1) val fact : int -> int = <fun> ou encore, syntaxiquement plus léger : let rec fact = function 0 -> 1 n -> n * fact (n-1) val fact : int -> int = <fun> 1.3. L implication. let implique p q = (not p) q val implique : bool -> bool -> bool = <fun> La fonction implique peut aussi être définie comme suit (d après sa table de vérité) : let implique p q = match (p,q) with true, true -> true true, false -> false false, true -> true false, false -> true val implique : bool -> bool -> bool = <fun> que l on peut réduire à : let implique p q = match (p,q) with true, false -> false _ -> true val implique : bool -> bool -> bool = <fun> Remarque. La définition d une fonction à l aide du mot-clef function autorise le pattern matching, mais sans currifycation (autrement dit : avec un seul argument, dont la valeur est filtrée; voir le troisième exemple à la section 1.2 ci-dessus). A l inverse, l emploi du mot-clef fun autorise la curryfication, mais sans pattern matching. Par exemple, la construction suivante est incorrecte : let implique = fun true false -> false _ -> true true false -> false ^ Error: Syntax error On s en sort en effectuant un filtrage sur un couple (ou, plus généralement, un multiplet) : voit l exemple qui précède cette remarque.
OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 3 1.4. Jeu de cartes. Ce qui suit utilise la notion de type somme (qui sera présentée dans un document ultérieur) : type couleur = Pique Coeur Carreau Trefle type carte = As of couleur Roi of couleur Dame of couleur Valet of couleur Petite_carte of int * couleur let valeur couleur_atout = function As _ -> 11 Roi _ -> 4 Dame _ -> 3 Valet c -> if c = couleur_atout then 20 else 2 Petite_carte (10, _) -> 10 Petite_carte (9, c) -> if c = couleur_atout then 14 else 0 _ -> 0 val valeur : couleur -> carte -> int = <fun> valeur Pique (Valet Trefle) - : int = 2 valeur Pique (Valet Pique) - : int = 20 1.5. Filtrage d intervalles de caractères. OCaml autorise la syntaxe abrégée c 1..c n pour un motif du type c 1 c 2... c n (où les c i sont des caractères consécutifs du code ASCII) : let categorie = function a e i o u y A E I O U Y -> "voyelle" a.. z A.. Z -> "consonne" 0.. 9 -> "chiffre" _ -> "autre" val categorie : char -> string = <fun> Evidemment, il y a parmi les lettres de a à z autre chose que des consonnes, mais rappelons que lors d un filtrage par motif, le premier motif qui convient est retenu. Ceci explique le bon fonctionnement de cet exemple. categorie A - : string = "voyelle" categorie t - : string = "consonne" categorie 6 - : string = "chiffre" categorie é - : string = "autre"
4 OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 1.6. Filtrage avec gardes. OCaml permet d agrémenter un motif de filtrage d une condition booléenne faisant intervenir les variables du motif : 2 1 K2 K1 0 1 2 let f = function x when x >= 1. -> 1. x when x >= 0. -> x _ -> 0. K1 K2 Autre exemple : let g = function (x,0) -> 0 (0,y) -> 0 (x,y) when x < y -> x _ -> 1 val g : int * int -> int = <fun> Remarque. Pour des raisons de clarté, on évitera toutefois le filtrage avec gardes. A n employer que si on ne voit vraiment pas comment s en sortir autrement. 1.7. Un motif ne peut contenir deux fois la même variable! let f a b = match (a, b) with 0, 0 -> 0 x, x -> 1 _ -> 2 Toplevel input: > x x -> 1 > ^ Error: Variable x is bound several times in this matching On peut s en sortir en utilisant un filtrage avec garde : let f a b = match (a, b) with 0, 0 -> 0 x, y when x = y -> 1 _ -> 2 val f : int -> int -> int = <fun> f 0 0 - : int = 0 f 4 4 - : int = 1 f 4 5 - : int = 2
1.8. D autres syntaxes pour le filtrage. OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 5 (1) La construction let... in..., que l on a déjà rencontrée, utilise aussi le filtrage : let x = 3 in 2*x - : int = 6 Dans cet exemple, le motif x filtre la valeur 3 puis l identificateur x est liée à 3. let (x,y,_) = (10,20,50) in x + y - : int = 30 Cette fois, le motif (x,y,_) filtre la valeur (10,20,50), puis les identificateurs x et y sont respectivement liées à 10 et 20 (et la valeur 50 est perdue...). (2) Les motifs alias : let f = function (0,_,_) as m -> m (_,_,z) -> (z,z,z) val f : int * int * int -> int * int * int = <fun> La construction motif as nom permet d éviter d avoir à réécrire toute une expression en la nommant globalement. Autre exemple : let g = function ((0,0), ((_,_) as m)) -> m (((_,_) as m), (0,0)) -> m (((x,y) as m), ((x,y ) as m )) -> if x < x then m else m val g : (int * int) * (int * int) -> int * int = <fun> (3) Enfin, le rattrapage d exceptions (voir section 3 ci-dessous) utilise aussi le filtrage. 2. Polymorphisme Lorsque OCaml calcule le type d une expression (par exemple une fonction), c est le type le plus général possible qui est adopté. Ceci permet notamment de définir des fonctions agissant sur des données de types variés. Par exemple, on pourra écrire une fonction qui trie un vecteur, qu il s agisse d un vecteur d entiers ou de flottants 1. Ainsi, OCaml autorise certains types à rester indéterminés : c est un langage polymorphe. On a déjà rencontré des fonctions polymorphes : fst et snd, Array.length,... D autres exemples seront rencontrés à l occasion de l étude des listes (cf. poly 5). 2.1. La fonction identité générique. let id x = x val id : a -> a = <fun> id 19 - : int = 19 id "hop" - : string = "hop" Le type de l argument x est a (lire α), c est-à-dire n importe quel type! On dit que id est une fonction polymorphe et que a est une variable de type. 1.... ou de n importe quoi d autre, pourvu qu on dispose d une fonction de comparaison appropriée.
6 OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 2.2. Les fonctions fst et snd. On a déjà rencontré ces deux fonctions prédéfinies dans un document antérieur. Voici la signature de fst : fst - : a * b -> a = <fun> On pourrait les redéfinir ainsi : ou encore : let fst (x,y) = x and snd (x,y) = y val fst : a * b -> a = <fun> val snd : a * b -> b = <fun> let fst (x,_) = x and snd (_,y) = y val fst : a * b -> a = <fun> val snd : a * b -> b = <fun> Dans le même esprit, voici comment définir les fonctions de sélection des composantes d un triplet : let premier (x,_,_) = x and second (_,x,_) = x and troisieme (_,_,x) = x val premier : a * b * c -> a = <fun> val second : a * b * c -> b = <fun> val troisieme : a * b * c -> c = <fun> premier (1,10,100) - : int = 1 second (1,10,100) - : int = 10 troisieme (1,10,100) - : int = 100 2.3. Echange de deux composantes d un vecteur. let echange v a b = let tmp = v.(a) in v.(a) <- v.(b); v.(b) <- tmp val echange : a array -> int -> int -> unit = <fun> Le vecteur v est de type a array : il s agit d un type paramétré. La fonction echange peut ainsi s appliquer à des vecteurs de divers types : let v = [ 1;4;7 ] val v : int array = [ 1; 4; 7 ] echange v 0 1 - : unit = () v - : int array = [ 4; 1; 7 ] let w = [ "Pierre";"est le frère de";"paul" ] in echange w 0 2; w - : string array = [ "Paul"; "est le frère de"; "Pierre" ]
OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 7 2.4. La loi o. La composition des fonctions est tout naturellement une fonctionnelle (cf. poly 0) : let compose f g x = f (g x) val compose : ( a -> b) -> ( c -> a) -> c -> b = <fun> OCaml laisse indéterminés les types des arguments de f et de g mais impose une condition compatibilité : le type de l argument de f est celui de la valeur de retour de g. Si l on veut disposer d un opérateur binaire de composition des fonctions, il suffit lui choisir un nom commençant par l un des caractères suivants : = @ ^ & + - * / $ % et éventuellement suivi d un (ou plusieurs) caractère(s) choisi(s) parmi @ ^ & + - * / $ % ~!. :? et de parenthéser son nom dans la définition. Par exemple : let (@@) f g x = f (g x) val ( @@ ) : ( a -> b) -> ( c -> a) -> c -> b = <fun> Il ne reste plus qu à essayer : let u x = x + 1 u : int -> int = <fun> let v x = x * x v : int -> int = <fun> let uv = u @@ v uv : int -> int = <fun> let vu = v @@ u vu : int -> int = <fun> (uv 5, vu 5) - : int * int = (26, 36) 2.5. Tri à bulles d un vecteur. L idée du tri à bulles (en anglais : bubble sort ) est de faire remonter les éléments les plus légers vers la surface... Lorsque le vecteur à trier est de longueur 1, il n y a rien à faire! Pour un vecteur de longueur n 2, on le parcourt de gauche à droite en échangeant deux éléments consécutifs v.(i) et v.(i+1) chaque fois que v.(i) est plus grand que v.(i+1). A l issue de ce parcours, l élément le plus grand se trouve bien placé, dans la case la plus à droite. Il ne reste plus qu à refaire la même chose avec le sous-vecteur v.(0..n-2). Voila pour le principe. Passons à l implémentation... Dans un premier temps, nous comparerons deux éléments à l aide de l opérateur binaire < (qui est un opérateur polymorphe : on pourra déjà trier des vecteurs d entiers, de flottants, de caractères ou de chaines de caractères). Une version plus générale sera envisagée ensuite. L échange de deux éléments d un vecteur se fera au moyen de la fonction echange, décrite à la section 2.3 ci-dessus.
8 OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS let tri_bulles v = let rec tri n = if n >= 2 then ( for i = 0 to n - 2 do if v.(i) > v.(i + 1) then echange v i (i + 1) done; (* le plus grand élément est maintenant à sa place *) tri (n-1); (* on recommence avec le sous-vecteur v(0..n-2) *) ) else v in tri (Array.length v) tri_bulles : a array -> a array = <fun> Passons aux essais : let v = Array.make 10 0 v : int array = [ 0; 0; 0; 0; 0; 0; 0; 0; 0; 0 ] for i = 0 to 9 do v.(i) <- Random.int (100) done - : unit = () v - : int array = [ 20; 73; 50; 61; 76; 17; 51; 62; 55; 36 ] tri_bulles v - : int array = [ 17; 20; 36; 50; 51; 55; 61; 62; 73; 76 ] Deux autres exemples (pour souligner la nature polymorphe de tri_bulles) : tri_bulles [ -1.23;-2.;3.14;-1.618;1.732;1.414 ] - : float array = [ -2.0; -1.618; -1.23; 1.414; 1.732; 3.14 ] tri_bulles [ "la";"vie";"est";"une";"fête" ] - : string array = [ "est"; "fête"; "la"; "une"; "vie" ] Venons-en à la version plus générale annoncée plus haut : il s agit de trier un vecteur dont les éléments sont de type arbitraire. Pour cela, il faut bien sûr disposer d une fonction de comparaison entre deux valeurs. La fonction tri_bulles précédente est modifiée de façon mineure : on lui passe un paramètre supplémentaire pgq (pour plus grand que ) qui est une fonction de type a -> a -> bool. Ce qui a changé est souligné : let tri_bulles_gene v pgq = let rec tri n = if n >= 2 then ( for i = 0 to n-2 do if pgq v.(i) v.(i+1) then echange v i (i+1) done; tri (n-1); ) else v in tri (Array.length v) tri_bulles_gene : a array -> ( a -> a -> bool) -> a array = <fun> A titre d exemple, trions un vecteur de chaines de caractères, en considérant qu une chaine s1 est supérieure à une chaine s2 si, et seulement si, la longueur de s1 dépasse strictement celle de s2 :
OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 9 let pgq s1 s2 = (String.length s1 > String.length s2) in tri_bulles_gene [ "Le";"capitaine";"Haddock";"a";"soif" ] pgq - : string array = [ "a"; "Le"; "soif"; "Haddock"; "capitaine" ] 3. Exceptions 3.1. De quoi s agit-il? L évaluation d une expression peut parfois échouer en raison d une circonstance particulière... 1 / 0 Exception: Division_by_zero let v = [ 0; 1; 2; 3; 4 ] in v.(5) Exception: Invalid_argument "index out of bounds". let s = "CAML" in s.[9] Exception: Invalid_argument "index out of bounds". List.hd [] Exception: Failure "hd". Pour comprendre le dernier exemple ci-dessus, voir le polycope n 5 (consacré aux listes). OCaml intègre un mécanisme permettant de produire (on dit aussi lever, ou raise en anglais) et d intercepter (on dit aussi rattraper, ou catch en anglais) des exceptions. L idée est la suivante : lorsqu une exception se produit, le calcul en cours est interrompu et l exception est propagée jusqu à ce qu elle soit éventuellement rattrapée, c est-à-dire gérée proprement, en quelque sorte. En cas d exception non rattrapée ( uncaught exception ), un message d erreur est affiché (comme dans les trois exemples ci-dessus). Les exceptions appartiennent à un type spécial (le type exn), qui comporte des constantes prédéfinies parmi lesquelles : Division_by_zero - : exn = Division_by_zero Out_of_memory - : exn = Out_of_memory Le type exn est extensible, au sens où le programmeur peut définir ses propres exceptions : exception Nombre_trop_grand (* Attention : la majuscule compte! *) exception Nombre_trop_grand Le constructeur Failure permet de générer une exception à l aide d un paramètre de type string : Failure catastrophe - : exn = Failure catastrophe 3.2. Comment lever une exception? La fonction raise effectue cette tâche : raise - : exn -> a = <fun> La fonction failwith permet une syntaxe simplifiée. Par exemple, l expression équivaut à failwith catastrophe
10 OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS raise (Failure catastrophe ) On aura noté que la fonction raise renvoie un type polymorphe : cela lui permet de s adapter à tous les contextes, comme le montrent les deux exemples suivants. let f = function 0. -> failwith "0 interdit" 1. -> failwith "1 interdit" x -> 1. /. (x *. (1. -. x)) f : float -> float = <fun> let rpo x v = (* Rang de la Première Occurrence de la valeur x dans le vecteur v *) let i = ref 0 and found = ref false and n = Array.length v in while (not!found) && (!i < n) do if v.(!i) = x then found := true else incr i done; if!i = n then failwith "Not found" else!i rpo : a -> a array -> int = <fun> Dans le premier de ces deux exemples, il FAUT que l expression failwith "0 : valeur interdite" soit de type float. Dans le second, il FAUT que l expression failwith "Not found" soit de type int. 3.3. Comment rattraper une exception? Avec la construction try... with... dont la syntaxe est la suivante : try e with m 1 -> e 1... m r -> e r Les m i sont des motifs de type exn et les e i sont des expressions ayant toutes le même type, à savoir celui de e. Si l évaluation de e ne déclenche pas d exception, la valeur de l ensemble de l expression est celle de e. Si en revanche une exception se produit, alors les motifs sont essayés l un après l autre : dès que l un d eux disons m i filtre cette exception, c est la valeur de e i qui est retenue. Prenons l exemple de la fonction f définie à la section précédente : f n est définie ni en 0 ni en 1, mais la fonction : f : x 1 x (1 x) g : x sin (2x) sin (3 (1 x)) f (x) est prolongeable par continuité en 0 et en 1 et les valeurs de prolongement sont respectivement 2 sin (3) et 3 sin (2). Voici comment on peut définir g en OCaml : let g x = try sin(2. *. x) *. sin (3. *. (1. -. x)) *. f(x) with Failure "0 interdit" -> 2. *. sin(3.) Failure "1 interdit" -> 3. *. sin(2.) g : float -> float = <fun>
OCAML - 3 / FILTRAGE / POLYMORPHISME / EXCEPTIONS 11 Considérons maintenant l exemple de la fonction rpo définie à la section précédente et définissons une fonction subs telle que subs src v r effectue le remplacement de la première occurrence dans v de chaque terme de src par r : let subs src v r = let n = Array.length src in for i = 0 to n-1 do v.(rpo src.(i) v) <- r done; v subs : a array -> a array -> a -> a array = <fun> subs [ 1;2;3 ] [ 5;3;1;3;6;1;2 ] 0 - : int array = [ 5; 0; 0; 3; 6; 1; 0 ] subs [ 1;2;7 ] [ 5;3;1;3;6;1;2 ] 0 Uncaught exception: Failure "Not found" Pour rattraper le coup, on modifie comme suit la fonction subs : let subs src v r = let n = Array.length src in for i = 0 to n-1 do try v.(rpo src.(i) v) <- r with Failure "Not found" -> () done; v subs : a array -> a array -> a -> a array = <fun> subs [ 1;2;7 ] [ 5;3;1;3;6;1;2 ] 0 - : int array = [ 5;3;0;3;6;1;0 ] 0 Dans un document ultérieur, on verra comment le calcul du déterminant d une matrice carrée peut être programmé en utilisant une exception pour gérer le cas des matrices non inversibles.