VI. Traitement des exceptions▲
En programmation fonctionnelle, les fonctions sont totales, c'est-à-dire qu'elles sont applicables à tout argument qui appartient à leur type de départ. Il faut donc être capable de traiter les cas, appelés exceptions , où cet argument n'est pas acceptable. Par exemple : une division par zéro ou bien la recherche de la tête d'une liste vide. Ceci peut être fait par des tests préventifs placés dans le corps des fonctions, mais ce mécanisme est très lourd, car il implique un travail important pour le programmeur et il altère la lisibilité d'un programme en masquant son fonctionnement normal. C'est pourquoi les langages de programmation modernes comportent un mécanisme spécifique pour le traitement des exceptions. Dans ce chapitre nous étudions celui qui est offert par Caml.
VI-A. Déclenchement d'exceptions sans récupération▲
Considérons le programme suivant :
#let f x y = x / y;;
f : int -> int -> int = <fun>
#let g x y z = (x + y) / z;;
g : int -> int -> int -> int = <fun>
#(f 4 2) + (g 3 1 0);;
Uncaught exception: Division_by_zero
L'expression (f 4 2) + (g 3 1 0) n'a pas pu être calculée, car Caml a rencontré une division par 0 lors de l'application de g. Il a alors déclenché (on dit aussi « levé » ) l'exception Division_by_zero qui a été interceptée au niveau le plus haut (« top level » en anglais) ce qui a provoqué une interruption du programme signalée par le message Uncaught exception: Division_by_zero.
Ce mécanisme de détection des exceptions est tout à fait insuffisant pour les deux raisons suivantes :
- le message généré par Caml ne permet pas de savoir en quel point du programme l'exception a été déclenchée. Dans l'exemple ci-dessus on ne sait pas si la division par zéro s'est produite lors de l'application de f ou de celle de g ;
- le programme est interrompu brutalement sans qu'il soit possible de remédier à la cause de l'erreur pour que l'exécution puisse se poursuivre.
VI-B. Déclenchement d'exceptions avec récupération▲
Le mécanisme de traitement des exceptions de Caml peut être schématisé de la façon suivante (cf. Figure 6.1) :
Figure 6.1 : Le mécanisme de traitement des expressions de Caml
- Lorsque le programmeur désire remédier aux erreurs pouvant se produire lors de l'évaluation d'une expression il peut encapsuler celle-ci dans un bloc de récupération d ' exception et déclencher une exception (!!!) si une erreur se produit.
- Un bloc de récupération d'exceptions ( B 1 et B 2) est composé de l'expression à évaluer et d'une fonction applicable aux exceptions pouvant être déclenchées dans ce bloc ( e 1 et f 1 pour le bloc B 1, e 2 et f 2 pour le bloc B 2).
- Lorsqu'une exception x est déclenchée ( 1 ) l'évaluation en cours est interrompue. La fonction de récupération f du bloc englobant le plus imbriqué est appliquée à x . La valeur de cette application remplace celle de l'expression dont l'évaluation a été interrompue.
- Si f n'est pas applicable à x alors x est à nouveau déclenchée ( 2 ) et le processus recommence à l'étape 3. Si le niveau le plus haut est atteint Caml interrompt l'évaluation et indique que cette interruption a été causée par une exception non récupérée (Uncaught exception) ( 3 ).
VI-B-1. Définition d'une exception▲
Caml offre un type union prédéfini exn dont les instances sont des exceptions. Les constructeurs d'exceptions sont définis incrémentalement par le programmeur au fur et à mesure de ses besoins.
Une exception est définie par la phrase :
exception C of t;;
où C est le nom du constructeur de l'exception et t est une expression de type. Par exemple :
#exception Pile_vide;;
Exception Pile_vide defined.
#exception Erreur of string;;
Exception Erreur defined.
#Pile_vide;;
- : exn = Pile_vide
#Erreur "Division par zéro en appliquant g";;
- : exn = Erreur "Division par zéro en appliquant g"
Les valeurs Pile_vide et Erreur "Division par zéro en appliquant g" sont des instances du type exn.
Par ailleurs Caml possède un certain nombre d'exceptions prédéfinies : Failure, Match_Failure, Invalid_argument, etc.
VI-B-2. Déclenchement d'une exception▲
Le déclenchement d'une exception est réalisé :
- soit par l'évaluateur de Caml, c'est le cas des exceptions prédéfinies comme Division_by_zero dont nous avons donné un exemple au §VI.A ;
- soit par l'évaluation de l'expression :
raise e
- où raise est une fonction prédéfinie et e est une expression de type exn.
Dans le programme suivant, par exemple, on déclenche une exception lorsqu'une pile devient vide :
#type 'a pile = Pile of 'a list;;
Type pile defined.
#let depiler = function
| Pile (x::p) -> x
| Pile [] -> raise Pile_vide;;
depiler : 'a pile -> 'a = <fun>
#depiler (Pile [1; 2]);;
- : int = 1
#depiler (Pile []);;
Uncaught exception: Pile_vide
Il est important de noter que le type d'arrivée de la fonction raise est un paramètre de type. Vérifions-le :
#raise;;
- : exn -> 'a = <fun>
Ceci, afin que l'insertion d'un déclenchement d'exception dans une expression n'ait pas d'influence sur le type de cette expression. Par exemple, si dans la fonction suivante :
#function x -> 365 / x;;
- : int -> int = <fun>
on insère un déclenchement d'exception en cas de division par zéro, on constate que le type de la fonction n'est pas changé :
#function x ->
if x <> 0 then
365 / x
else
raise (Erreur "Division par zéro");;
- : int -> int = <fun>
Il existe des exceptions prédéfinies de la forme Failure m (où m est une chaîne de caractères) qui sont déclenchées par l'application de la fonction prédéfinie failwith au messsage m . Par exemple :
#let debiter compte montant =
if compte > montant then
compte - montant
else
failwith "Credit insuffisant";;
debiter : int -> int -> int = <fun>
#debiter 23 54;;
Uncaught exception: Failure "Credit insuffisant"
VI-B-3. Récupération d'une exception▲
La récupération d'une exception déclenchée lors de l'évaluation d'une expression e peut être réalisée en encapsulant cette expression dans une expression try..with qui a la forme suivante :
try e with F1 -> e1 | … | Fn -> en
où e , e 1 , …, en sont des expressions de même type et F 1 , …, Fn sont des filtres de type exn.
La valeur de cette expression est celle de e si aucune exception n'est déclenchée, sinon elle est celle de l'expression associée au premier des filtres F 1 , …, Fn qui filtre l'exception déclenchée.
La sémantique d'une récupération d'exception est la suivante.
TYPE ET VALEUR D'UNE RECUPERATION D'EXCEPTION. Si
Env
est un environnement et si
e
,
e
1
, …,
en
sont des expressions telles que
type
(e,
Env
) =
type
(
e
1
,
Env
) = … =
type
(
e
n
,
Env
) =
t
alors :
type (try e with F 1 -> e 1 | … | Fn -> e n , Env ) = t . Si aucune exception n'a été déclenchée lors de l'évaluation de e alors : val (try e with …, Env ) = val ( e , Env ) sinon, si une exception x a été déclenchée : val (try e with F 1 -> e 1 | … | Fn -> e n , Env ) = val (match x with F 1 -> e 1 | … | Fn -> en | _ -> raise x , Env ). |
En clair,
Par exemple, la fonction ajouter définie par :
#let ajouter n p = n + try depiler p with Pile_vide -> 0;;
ajouter : int -> int pile -> int = <fun>
ajoute à n le nombre placé au sommet de la pile d'entiers p. Dans le cas où la pile est vide, c'est zéro qui est ajouté à n . Vérifions-le :
#ajouter 5 (Pile [1]);;
- : int = 6
#ajouter 5 (Pile []);;
- : int = 5