• Aucun résultat trouvé

Fonctionnelles et polymorphisme

7.2 Les exceptions

Erreurs et rattrapage d’erreurs

Dans les langages fonctionnels, toute fonction qui ne boucle pas ind´efiniment doit rendre une valeur, quel que soit son argument. Malheureusement certaines fonctions, bien que n´ecessairement d´efinies pour toute valeur de leur type argument, ne peuvent pas retourner de valeur sens´ee pour tous les arguments possibles. Consid´erez par ex- emple la division entre nombres entiers : que doit-elle faire lorsqu’on tente de diviser par 0 ? Le probl`eme se pose aussi pour les donn´ees structur´ees : consid´erez la fonction t^etequi renvoie la tˆete d’une liste. Que peut-elle faire lorsque son argument est la liste vide ? Dans de telles situations la fonction doit ´echouer, c’est-`a-dire arrˆeter les calculs et signaler une erreur. C’est ce que nous avons fait en utilisant la fonction pr´ed´efinie failwith:

# failwith;;

- : string -> ’a = <fun>

qui envoie un message indiquant la cause de l’´echec. C’est pourquoi nous d´efinissons t^ete par :

# let t^ete = function | [] -> failwith "t^ete" | x::_ -> x;;

t^ete : ’a list -> ’a = <fun>

Et maintenant, t^ete []nous signale une erreur dans la fonction t^ete:

# t^ete [];;

Exception non rattrap´ee: Failure "t^ete"

Ce m´ecanisme de d´eclenchement d’erreurs est utile, mais il se peut que nous voulions r´ecup´erer ces erreurs, parce que nous savons comment continuer les calculs apr`es une telle erreur (qui devient une erreur « attendue » du point de vue du programmeur). Par exemple, imaginons qu’on doive ajouter syst´ematiquement la tˆete d’une liste `a un compteur. Si la liste est vide, il est logique de continuer les calculs en n’ajoutant rien au compteur. Dans ce cas, l’´echec signal´e par la fonction t^etedoit ˆetre r´ecup´er´e. On utilise pour cela la construction try . . . with . . . (try signifie essayer et with avec) qui permet de calculer une expression en surveillant les exceptions que son calcul peut d´eclencher. Cette construction ob´eit `a la syntaxe suivante : try expression with filtrage. Elle signifie intuitivement : essayer de calculer la valeur de expression et si cette ´evaluation d´eclenche une erreur qui tombe dans un des cas du filtrage alors retourner la valeur correspondante de la clause s´electionn´ee par le filtrage. Par exemple, puisque l’erreur signal´ee par la fonction t^ete est Failure "t^ete", on envisagera cet ´echec dans la partie filtrage du try. . . with . . . pour renvoyer une valeur enti`ere, comme si aucune erreur n’avait ´et´e d´eclench´ee. On rattrape donc l’´echec sur la liste vide et l’on renvoie 0, par la phrase :

# try (t^ete []) with Failure "t^ete" -> 0;; - : int = 0

Les exceptions 127 On ´ecrira donc la proc´edure d’incr´ementation du compteur :

# let ajoute_au_compteur compteur l =

compteur := !compteur + (try (t^ete l) with Failure "t^ete" -> 0);; ajoute_au_compteur : int ref -> int list -> unit = <fun>

# let c = ref 0;; c : int ref = ref 0

# ajoute_au_compteur c [1]; !c;; - : int = 1

# ajoute_au_compteur c []; !c;; - : int = 1

C’est la m´ethode ´el´ementaire d’utilisation des exceptions de Caml. Nous d´ecrivons maintenant le m´ecanisme dans toute sa g´en´eralit´e.

Valeurs exceptionnelles

Le trait distinctif du traitement d’erreurs en Caml, et ce qui en fait la g´en´eralit´e, est le statut des erreurs : ce sont des valeurs `a part enti`ere du langage. Elles appartiennent `

a un type pr´ed´efini exn et on les appelle « valeurs exceptionnelles ». On les manipule donc comme toutes les autres valeurs. Par exemple, l’´echec signal´e par la fonction t^ete est la valeur exceptionnelle Failure "t^ete":

# let ´echec_de_t^ete = Failure "t^ete";; ´

echec_de_t^ete : exn = Failure "t^ete"

Remarquez que Failure n’est rien d’autre qu’un constructeur de valeurs du type exn. La propri´et´e caract´eristique des valeurs exceptionnelles est ´evidemment qu’on peut les d´eclencher (on dit aussi les lever, par analogie avec la locution « lever une perdrix »). Pour lever une valeur exceptionnelle on utilise la fonction pr´ed´efinie raise (en anglais to raise signifie « lever ») :

# raise;;

- : exn -> ’a = <fun> # raise ´echec_de_t^ete;;

Exception non rattrap´ee: Failure "t^ete"

La primitive raise est une fonction « magique », car elle n’est pas d´efinissable dans le langage. Elle interrompt imm´ediatement les calculs en cours pour d´eclencher le signal (lever la valeur exceptionnelle) qu’elle a re¸cu en argument. C’est ce qui explique qu’un appel `a la fonction raise puisse intervenir dans n’importe quel contexte avec n’importe quel type : les calculs ne seront de toute fa¸con jamais effectu´es lorsqu’on ´evaluera l’appel `

a raise, le contexte peut donc faire toutes les hypoth`eses qu’il d´esire sur la valeur renvoy´ee par raise. Par exemple :

# 1 + (raise ´echec_de_t^ete);;

Exception non rattrap´ee: Failure "t^ete" # "Bonjour" ^ (raise ´echec_de_t^ete);; Exception non rattrap´ee: Failure "t^ete"

Bien entendu, les phrases essentiellement mal typ´ees, o`u raise apparaˆıt dans un con- texte lui-mˆeme mal typ´e, sont toujours rejet´ees :

# 1 + (raise ´echec_de_t^ete) ^ "Bonjour";; Entr´ee interactive:

>1 + (raise ´echec_de_t^ete) ^ "Bonjour";; >^^^^^^^^^^^^^^^^^^^^^^^^^

Cette expression est de type int, mais est utilis´ee avec le type string.

La construction try ... with

On peut donc consid´erer les valeurs exceptionnelles comme des signaux qu’on envoie `

a l’aide de la fonction raise et qu’on re¸coit avec la construction try . . . with . . . La s´emantique de try e with filtrage est de retourner la valeur de e si e s’´evalue

«normalement », c’est-`a-dire sans d´eclenchement d’exception. En revanche, si une valeur exceptionnelle est d´eclench´ee pendant l’´evaluation de e, alors cette valeur est filtr´ee avec les clauses du filtrage et comme d’habitude la partie expression de la clause s´electionn´ee est renvoy´ee. Ainsi, la partie filtrage de la construction try . . . with . . . est un filtrage parfaitement ordinaire, op´erant sur des valeurs du type exn. La seule diff´erence est qu’en cas d’´echec du filtrage, la valeur exceptionnelle qu’on n’a pas pu filtrer est propag´ee, c’est-`a-dire d´eclench´ee `a nouveau. Comparez ainsi une ´evaluation habituelle :

# try t^ete [1] with Failure "t^ete" -> 0;; - : int = 1

une ´evaluation d´eclenchant une valeur exceptionnelle rattrap´ee :

# try t^ete [] with Failure "t^ete" -> 0;; - : int = 0

et finalement une propagation de valeur exceptionnelle :

# try t^ete [] with Failure "reste" -> 0;; Exception non rattrap´ee: Failure "t^ete"

D´efinition d’exceptions

De nombreuses fonctions pr´ed´efinies de Caml, quand elles ´echouent, d´eclenchent l’exception Failure avec leur nom comme argument. C’est pourquoi l’exception Failure poss`ede un « d´eclencheur » pr´ed´efini, la fonction failwith. Nous pouvons maintenant ´ecrire sa d´efinition :

# let failwith s = raise (Failure s);; failwith : string -> ’a = <fun>

Si les exceptions pr´ed´efinies ne vous satisfont pas, parce que vous souhaitez par exemple que votre valeur exceptionnelle transporte autre chose qu’une chaˆıne de car- act`eres, vous pouvez d´efinir une nouvelle exception. En effet, le type exn est un type somme (il y a plusieurs exceptions diff´erentes ; c’est donc un type « ou »), mais d’un genre tr`es particulier : sa d´efinition n’est jamais achev´ee. C’est pourquoi il est possible `

a tout moment de lui ajouter de nouveaux constructeurs, soit constants soit fonction- nels. Pour d´efinir un nouveau constructeur du type exn, donc une nouvelle exception, on utilise le mot-cl´e exception suivi d’une d´efinition de constructeur de type somme. Pour d´efinir la nouvelle exception constante Stop, on ´ecrira donc simplement :

Les exceptions 129

# exception Stop;;

L’exception Stop est d´efinie.

La d´efinition d’une exception fonctionnelle comportera une partie « of type » qui pr´ecise le type de l’argument de l’exception.

# exception Erreur_fatale of string;; L’exception Erreur_fatale est d´efinie.

# raise (Erreur_fatale "Cas impr´evu dans le compilateur");;

Exception non rattrap´ee: Erreur_fatale "Cas impr´evu dans le compilateur"

Voici la description pr´ecise des d´efinitions d’exception `a l’aide de diagrammes syntax- iques :

D´efinition d’exceptions ::= exception d´efinition-de-constructeur (and d´efinition-de-constructeur)∗

d´efinition-de-constructeur ::= identificateur

| identificateur of type

Les exceptions comme moyen de calcul

Les exceptions ne servent pas seulement `a g´erer les erreurs : elles sont aussi utilis´ees pour calculer. Dans ce cas, la valeur exceptionnelle transporte un r´esultat, ou bien signale un ´ev´enement attendu. `A titre d´emonstratif, nous d´efinissons la fonction caract`ere_dans_cha^ıne, qui teste l’appartenance d’un caract`ere `a une chaˆıne et dont nous avons besoin pour impl´ementer Cam´elia. On pourrait ´evidemment ´ecrire cette fonction `a l’aide d’une fonction r´ecursive locale :

# let caract`ere_dans_cha^ıne cha^ıne car = let rec car_dans_cha^ıne i =

i < string_length cha^ıne && (cha^ıne.[i] = car ||

car_dans_cha^ıne (i + 1)) in car_dans_cha^ıne 0;;

caract`ere_dans_cha^ıne : string -> char -> bool = <fun>

Cependant, cette fonction r´ecursive code ´evidemment une boucle ; nous pr´ef´erons donc l’´ecrire avec une boucle. On parcourt donc la chaˆıne argument `a l’aide d’une boucle foren recherchant le caract`ere donn´e. Cependant, que faire si le caract`ere est trouv´e ? Il faut arrˆeter la boucle et signaler sa pr´esence. Ce comportement revient `a d´eclencher une exception. Nous d´efinissons donc l’exception Trouv´e. Et nous surveillons la boucle de recherche : si l’exception est d´eclench´ee, la fonction renvoie true. En revanche, si la boucle se termine normalement, c’est que le caract`ere n’´etait pas dans la chaˆıne ; dans ce cas, on renvoie false en s´equence.

# exception Trouv´e;;

L’exception Trouv´e est d´efinie.

# let caract`ere_dans_cha^ıne cha^ıne car = try

for i = 0 to string_length cha^ıne - 1 do if cha^ıne.[i] = car then raise Trouv´e done;

with Trouv´e -> true;;

caract`ere_dans_cha^ıne : string -> char -> bool = <fun>

Ici le d´eclenchement de l’exception n’est pas un cas d’erreur, mais plutˆot un ´ev´enement heureux : on a d´etect´e la pr´esence du caract`ere dans la chaˆıne. On ne peut pas dire non plus que ce soit vraiment un ´ev´enement exceptionnel, une « exception » au calcul normal : c’est un signal attendu, tout simplement.

Sans le m´ecanisme des exceptions la fonction pr´ec´edente devrait ˆetre ´ecrite avec une r´ef´erence initialis´ee `a false en d´ebut de boucle et mise `a true lorsqu’on rencontre le caract`ere.

# let car_dans_cha^ıne cha^ıne car = let trouv´e = ref false in

for i = 0 to string_length cha^ıne - 1 do if cha^ıne.[i] = car then trouv´e := true done;

!trouv´e;;

car_dans_cha^ıne : string -> char -> bool = <fun>

Cette version est un peu moins efficace, puisque le parcours de la chaˆıne est toujours effectu´e compl`etement, alors qu’il est inutile de le continuer d`es qu’on a d´etect´e la pr´esence du caract`ere. Cet argument d’efficacit´e est minime : le choix entre les deux versions est essentiellement une affaire de goˆut personnel, de style et d’exp´erience. Nous pr´ef´erons la version avec exception, car elle se g´en´eralise plus facilement `a plusieurs ´ev´enements attendus dans la boucle. Au surplus, la boucle s’arrˆete instantan´ement quand l’´ev´enement arrive et c’est tr`es souvent un comportement algorithmiquement n´ecessaire du programme.