Programmation fonctionnelle Notes de cours Cours 5 31 Octobre 2012 Sylvain Conchon [email protected] 1/21 Le programme de cette semaine Le mode T9 des téléphones portables 2/21 Les notions abordées avec ce programme I Exceptions I Dictionnaires I Arbres de préfixes 3/21 Les exceptions # 1 / 0 ;; Exception: Division by zero. # let x = [| 1 |];; val x : int array = [| 1 |] ;; # x.(1);; Exception: Invalid argument "index out of bounds". # int of string "bonjour" ;; Exception: Failure "int of string". I Le mécanisme d’exception permet de gérer le problème des fonctions partielles : une exception est levée si aucune valeur ne peut être renvoyée par la fonction. I Si l’exception n’est pas rattrapée (voir ci-après), le programme se termine. 4/21 Les exceptions : déclaration et typage # exception Fin ;; exception Fin # raise Fin; print int 10 ;; Exception: Fin. # exception E of int ;; exception E of int # let f x = if x = 0 then raise (E(x)) else 10 / x ;; val f : int -> int = <fun> # f 0 ;; Exception: E(0) I Une exception est définie à l’aide du mot-clé exception. I Comme pour les constructeurs, elle commence par une majuscule et elle peut attendre des arguments. I On lève une exception à l’aide de la fonction raise. I Ce mécanisme est transparent au typage. 5/21 Les exceptions : rattrapage # let f x = if x = 10 then raise (E(10)) else 10 / x ;; # try f 0 with | Division by zero -> 0 | E x -> x ;; - : int = 0 I Une exception peut être rattrapée à l’aide de la construction try-with qui permet de définir des gestionnaires d’exceptions. I Comme pour un pattern-matching, un gestionnaire permet de récupérer les arguments associés aux exceptions. 6/21 Dictionnaires Un dictionnaire est une structure de données qui permet d’associer des clefs (de type ’a) à des valeurs (de type ’b). La manière la plus simple de réaliser un dictionnaire est d’utiliser une liste de paires (’a * ’b) list. La bibliothèque OCaml fournit un certain nombre de fonctions permettant de manipuler de tels dictionnaires. En particulier, I la fonction List.assoc renvoie la valeur associée à une clef (elle lève l’exception Not found si aucune valeur n’est associée) ’a -> (’a * ’b) list -> ’b I la fonction List.remove assoc supprime la première association liée à une clef ’a -> (’a * ’b) list -> (’a * ’b) list 7/21 Dictionnaires : changement d’association Pour changer l’association liée à une clef dans un dictionnaire, on définit la fonction change assoc de type ’a -> ’b -> (’a * ’b) list -> (’a * ’b) list de sorte que change assoc i v l change l’association liée à i dans la liste l par le couple (i, v) let rec change assoc i v l = match l with | [] -> [ (i, v) ] | (x, )::l when x=i -> (i, v)::l | z::l -> z::(change assoc i v l) 8/21 Les arbres de préfixes Structure de données pour représenter des ensembles de mots . Par mots, il faut comprendre toute valeur ocaml pouvant être décomposée comme une suite de lettres : I les valeurs de type string sont des mots dont les lettres sont de type char I les entiers de type int sont également des mots dont les lettres peuvent être simplement les chiffres 0 et 1 I etc. 9/21 Décomposition On utilise cette décomposition en lettres pour représenter des ensembles de mots de la manière suivante : I chaque branche est étiquetée par une lettre I chaque nœud contient un booléen qui indique si la séquence de lettres menant de la racine de l’arbre à ce nœud est un mot appartenant à l’ensemble 10/21 Exemple L’arbre de préfixes correspondant au dictionnaire {if, in, do, done} false i d false false n true f true o true n false e true 11/21 Pourquoi une telle structure ? Le temps de recherche d’un élément dans un arbre de préfixes est borné à la longueur du mot le plus long de cet ensemble, quelque soit le nombre de mots qu’il contient. I Cette propriété est garantie si toutes les feuilles d’un arbre de préfixes représentent bien un mot de l’ensemble, c’est-à-dire si elles contiennent toutes une valeur booléenne à vrai. 12/21 L’interface du module Trie type ’a t type ’a word = ’a list val empty : ’a t val is empty : ’a t -> bool val mem : ’a word -> ’a t -> bool val add : ’a word -> ’a t -> ’a t val remove : ’a word -> ’a t -> ’a t val inter : ’a t -> ’a t -> ’a t val union : ’a t -> ’a t -> ’a t val cardinal : ’a t -> int I ’a t est le type (abstrait) des arbres de préfixe dont les lettres sont de type ’a I les mots sont représentés par des listes de lettres ’a word 13/21 Implémentation du module Trie type ’a t = { is a word : bool ; branches : (’a * ’a t) list } Les valeurs de ce type sont les nœuds des arbres. I Le champ is a word contient la valeur booléenne indiquant la présence d’un mot dans l’arbre. I Le champ branches contient les fils d’un nœud. Il s’agit d’une liste d’associations qui associe des sous-arbres à des lettres. 14/21 Implémentation du module Trie I L’arbre de préfixes vide empty est représenté par un arbre réduit à un unique nœud où le champ is a word vaut false et branches est une liste vide : let empty = { is a word = false; branches = [] } I La fonction is empty pour tester qu’un arbre de préfixes est vide est alors définie de la manière suivante : let is empty t = not t.is a word && t.branches = [] 15/21 Recherche d’un élément let rec mem x t = match x with | [] -> t.is a word | i::l -> try mem l (List.assoc i t.branches) with Not found -> false La recherche d’un élément x procède récursivement de la façon suivante : I si le mot x est la liste vide, la recherche se termine en renvoyant la valeur booléenne associée à la racine de l’arbre t I Sinon, la recherche se poursuit récursivement dans le sous-arbre associé à la branche étiquetée par la première lettre i du mot x. Dans le cas où cette branche n’existe pas, la recherche se termine immédiatement sur un échec. 16/21 Insertion d’un élément let rec add x t = match x with | [] -> { t with is a word = true } | i::l -> let b = try List.assoc i t.branches with Not found -> empty in { t with branches = change assoc i (add l b) t.branches } 17/21 Insertion d’un élément L’insertion d’un mot x dans un arbre de préfixes t consiste à descendre le long de la branche étiquetée par les lettres de x. I Si x est vide, on renvoie un arbre t avec un champ is a word à vrai afin d’indiquer que x appartient désormais à cet ensemble. I Si x est de la forme i::l, on ajoute récursivement l dans le sous-arbre b associé à lettre i dans t. Si b n’existe pas, on réalise l’ajout à partir d’un arbre vide. L’insertion se termine en associant l’arbre ainsi obtenu à la lettre i. 18/21 Suppression d’un élément let rec remove x t = match x with | [] -> { t with is a word = false } | i::l -> try let s = remove l (List.assoc i t.branches) in let new branches = if is empty s then remove assoc i t.branches else change assoc i s t.branches in { t with branches = new branches } with Not found -> t 19/21 Suppression d’un élément La suppression d’un élément x dans un arbre t procède encore une fois avec le même parcours récursif que pour l’insertion. I La principale subtilité de cette opération est de maintenir la bonne formation de l’arbre renvoyé. Si x est la liste vide, il est supprimé de t simplement en passant le champ is a word à faux. I Sinon, x est de la forme i::l on procède ainsi : I I I On commence par supprimer récursivement le reste du mot l dans le sous-arbre associé à i. Si ce sous-arbre n’existe pas, la fonction se termine en renvoyant directement t. Puis, selon que l’arbre s ainsi obtenu est vide ou non, on supprime la branche associée à i dans t ou on lui associe s. Cela permet de garantir qu’aucune branche de t ne pointe vers un arbre vide. Enfin, la fonction se termine en associant ces nouvelles branches à t. 20/21 Cardinal let cardinal t = let n = ref 0 in let rec count t = if t.is a word then incr n; List.iter (fun ( , t) -> count t) t.branches in count t; !n I La fonction cardinal renvoie le nombre de mot contenu dans un arbre de préfixe. I Il s’agit simplement d’un parcours en profondeur d’un arbre pendant lequel on compte le nombre de nœuds ayant un champ is a word à true. 21/21