Exceptions

Transcription

Exceptions
$
'
Exceptions
ENSIIE - 1ère année - IAP1
C. Dubois
&
0
%
$
'
Mécanisme de traitement d’exceptions
Un mécanisme de traitement des exceptions permet de réaliser trois
sortes d’actions :
• déclarer une exception (si elle n’est pas prédéfinie) ;
• déclencher (lever ou signaler) une exception ;
• traiter (rattraper) une exception.
&
1
%
$
'
Exceptions prédéfinies / Exceptions utilisateur
En OCaml, une exception est une valeur à part entière.
⇒ une exception a un type : exn
Il existe des exceptions prédéfinies, de type exn :
Division by zero
Not found
Failure s où s est de type string
Match failure t où t est de type string*int*int
&
2
%
'
#let inv x = if x = 0. then failwith "inv indefini" else 1./.x;;
val inverse : float -> float = <fun>
# inverse 0.5;;
- : float = 2.
# inverse 0.;;
Exception: Failure "inverse : indefini".
$
#let f l = match l with
[x] -> "cas 1" | x::r -> "cas 2";;
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched: []
val f : ’a list -> string = <fun>
# f [];;
Exception: Match_failure ("", 1, 10).
# List.assoc 4 [(3,"toto"); (5, "titi")];;
Exception: Not_found.
&
3
%
$
'
Le programmeur peut définir ses propres valeurs de type exn
⇒ exceptions définies par le programmeur
Attention aux majuscules aux exceptions : les exceptions sont en fait
les constructeurs d’un type somme exn extensible
C’est comme si on avait :
type exn =
| Division_by_zero
| Not_found
| Failure of string
| Match_failure of string*int*int
| ....
&
4
%
$
'
Déclaration d’une exception
Cela revient à déclarer un nouveau constructeur pour le type exn.
Pour déclarer une exception non paramétrée de nom Identificateur,
on écrira :
exception Identificateur;;
Pour déclarer une exception paramétrée de nom Identificateur, on
écrira :
exception Identificateur of type_du_paramètre ;;
&
5
%
$
'
Exemples :
#exception Stop;;
Exception Stop
#exception Débit of float;;
Exception Débit of float
#Stop;;
- : exn = Stop
#Débit 10.5;;
- : exn = Débit 10.5
#Failure "exception";;
- : exn = Failure "exception"
#[Stop ; Failure "toto" ; Débit 10.5];;
- : exn list = [Stop; Failure "toto"; Débit 10.5]
&
6
%
$
'
Levée d’une exception
levée = signal = déclenchement = to raise
On utilise la fonction prédéfinie magique raise de type exn -> ’a
Syntaxe : Lever l’exception e ⇒ raise e
Typage : e doit être une expression de type exn.
raise e a n’importe quel type.
(raise Stop) + 1 : int et (raise Stop) ^ "toto" : string
raise Stop;;
raise (Débit 10.5);;
raise (Failure "problème");;
exception: Stop
exception: Débit 10.5
exception: Failure "problème"
failwith "problème" abbréviation pour raise (Failure "problème").
&
7
%
$
'
Sémantique :
Lorsqu’une exception est signalée, elle interrompt immédiatement tous
les calculs en cours.
La fonction raise ne retourne jamais de valeur.
1+(raise Stop);;
exception: Stop
l’exception Stop étant levée, l’addition ne sera pas
pas effectuée
De manière générale, l’utilisation sera la suivante :
if il_y_a_un_problème
then raise exception_prédéfinie_ou_exception_utilisateur
else traitement_non_exceptionnel
et ceci avant que le problème ne se produise réellement !!!!!!!
&
8
%
$
'
Autre forme fréquente d’utilisation : levée d’une exception dans une
des branches d’un filtrage
exception Liste_vide;;
let t^
ete l = match l with
[] -> raise Liste_vide
| e::_ -> e;;
t^
ete [];;
exception: Liste_vide
t^
ete [2; 3];;
&
- : int = 2
9
%
'
let t^
ete l = match l with
| [] -> failwith "t^
ete : liste vide"
| x::l’ -> x;;
$
(*interface inverse_tete
type int list -> int
arg l
precondition : l est une liste non vide et sa t^
ete est non nulle
postcondition : retourne l’inverse de la t^
ete de l
raises : Division_by_zero / Failure "t^
ete : liste vide" *)
let inverse_t^
ete l = 1 / (t^
ete l);;
inverse_t^
ete [1; 2; 3];;
inverse_t^
ete [];;
inverse_t^
ete [0];;
&
10
%
$
'
Traitement d’une exception
Il s’agit de récupérer l’exception pour, par exemple :
• terminer proprement l’exécution anormale
• ou entrer dans un mode dégradé mais de façon harmonieuse.
On écrit :
try expression with exception 1 -> expression 1
| exception 2 -> expression 2
...
| exception k -> expression k ;;
traite-exception ou encore handler d’exception.
⇒ remplacement du traitement usuel par un traitement particulier
&
11
%
$
'
Typage :
L’expression de traitement d’exception a le type t si exception 1,
exception 2, . . . exception n sont des filtres d’exception de type exn
et si expression, expression 1, expression 2, . . . , expression n ont
toutes le type t
Sémantique :
Si l’évaluation de expression se termine normalement, soit v sa valeur,
le traitement d’exception a alors la valeur v
Si l’évaluation de expression déclenche une exception e, si
exception j est le premier filtre d’exception pouvant être mis en
correspondance avec l’exception e, alors la valeur du traitement
d’exception est de celle de expression j.
Si aucun filtre d’exception ne convient, le traitement d’exception
échoue en levant l’exception e
&
12
%
$
'
Rendons la fonction inverse t^
ete plus robuste :
type nombre_étendu = Normal of int | Rien | Infini;;
(*interface inverse_tete
type int list -> nombre_étendu
arg l
precondition : true
postcondition : retourne l’inverse de la t^
ete de l si l est non vide
et sa tete n’est pas nulle, rien si l est vide et Infini si la tete
est nulle *)
let inverse_t^
ete l =
try
Normal (1/(t^
ete l))
with
| Failure "t^
ete : liste vide" -> Rien
| Division_by_zero -> Infini;;
&
inverse_t^
ete [1; 2; 3] ??
inverse_t^
ete [] ??
13
%
inverse_t^
ete [0] ??
'
Il est possible de ne rattraper qu’une seule des exceptions déclenchées par le
calcul 1/(t^
ete l), par exemple l’exception Failure "t^
ete : liste vide",
mais la fonction ne sera plus une fonction totale :
$
(*interface inverse_tete
type int list -> nombre_étendu
arg l
precondition : l liste vide ou si vide ne commençant pas par 0
postcondition : retourne l’inverse de la t^
ete de l si l est non vide
et sa tete n’est pas nulle, rien si l est vide
raises : échoue avec l’exception Division_by_zero si l est non vide
avec un premier élément nul *)
let inverse_t^
ete l =
try Normal (1/(t^
ete l)) with
| Failure "t^
ete : liste vide" -> Rien;;
inverse_t^
ete [1; 2; 3];;
inverse_t^
ete [];;
inverse_t^
ete [0];;
&
- : nombre_étendu = Normal 1
- : nombre_étendu = Rien
exception : Division_by_zero
14
%
$
'
Tout se passe comme si l’exception Division by zero était déclenchée
à nouveau.
On dit qu’elle est implicitement propagée
La version précédente est donc équivalente à :
let inverse_t^
ete l =
try Normal (1/(t^
ete l)) with
| Failure "t^
ete : liste vide"
| ex -> raise ex;;
&
15
-> Rien
%
$
'
En OCaml, la propagation des exceptions est implicite. On propage
l’exception à la fonction appelante. La recherche du handler associé à
une exception est dite dynamique.
Où placer le try ...with ... ?
Dans la fonction qui a levé l’exception ? Dans une des fonctions
appelantes ?
⇒ Pas de réponse universelle, sa place dépend du problème traité ainsi
que de la nature de l’exception levée.
&
16
%
$
'
#exception Absent of int;;
Exception Absent
#let rec trouve_nom numéro amphi =
match amphi with
| [] -> raise (Absent numéro)
| (x,n)::reste -> if n=numéro then x
else trouve_nom numéro reste;;
val trouve_nom : int -> (’a * int) list -> ’a = <fun>
#let verif (numéro, amphi) =
try (trouve_nom numéro amphi) with
| Absent p -> "chercher_à_la_scolarité";;
val verif : int * (string * int) list -> string = <fun>
&
17
%
$
'
exception Erreur of int;;
exception Signal of string;;
let f_qui_déclenche x =
if x < 0 then raise (Erreur (-x))
else if x=0
then raise (Signal "reprise au vol")
else x;;
f_qui_déclenche (-4);;
exception: erreur 4
f_qui_déclenche 2;;
- : int = 2
let récupère x = try f_qui_déclenche x with
| Erreur 0 -> 0
| Erreur y -> 25-y+3*x
| Signal _ -> 100*x+1;;
&
((récupère 2),(récupère (-4)),(récupère 0));;
18
(2, 9, 1)
%
$
'
exception Pas_grave of int ;;
exception Grave of string ;;
let pseudo_id x =
if (x < 0) then raise(Pas_grave(-x))
else if (x = 0) then raise(Grave "Argh !!!")
else x ;;
pseudo_id(-4);;
pseudo_id(0);;
pseudo_id(2);;
&
19
%
$
'
exception Pas_grave of int ;;
exception Grave of string ;;
let pseudo_id x =
if (x < 0) then raise(Pas_grave(-x))
else if (x = 0) then raise(Grave "Argh !!!")
else x ;;
pseudo_id(-4);;
Exception: Pas_grave 4
pseudo_id(0);;
Exception: Grave "Argh !!!"
pseudo_id(2);;
- : int = 2
&
20
%
$
'
let f = let v = 10 in
function x ->
try (let v = 100 in (pseudo_id x)-v) with
Pas_grave n -> if (n < 0)
then raise(Grave "impossible ?")
else n + v
| Grave "Argh !!!" -> raise(Pas_grave(10))
| Grave s -> raise(Grave "possible ?") ;;
Que valent les expressions f(-100), f 0, f 100, f(Grave "Ouf !!!")
et f(raise(Grave "Ouf !!!")) ?
&
21
%
$
'
#f(-100);;
- : int = 110
#f 0;;
Exception: Pas_grave 10
#f 100;;
- : int = 0
#f(Grave "Ouf !!!");;
>f(grave "Ouf !!!");;
> ^^^^^^^^^^^^^^^
Cette expression est de type exn, mais est utilisée avec
le type int.
#f(raise(Grave "Ouf !!!"));;
Exception: Grave "Ouf !!!"
&
22
%
$
'
Exception et évaluation
OCaml travaille avec l’appel par valeur
f e1 e2 ...
e1, e2 ...
en : tout d’abord calcul de f puis calcul des valeurs de
si ei échoue avec l’exception E alors toute l’application f e1 e2 ...
échoue avec l’exception E.
Idem pour un n-uplet (enregistrement, liste, valeur de type somme ...) :
dès qu’un composant lève une exception, le couple lève l’exception :
exemple : (1/0, 5+8)
if condition then e1 else e2 : si e1 échoue alors toute l’expression
échoue
let x = e1 in e2 : si e1 échoue alors toute l’expression échoue
&
23
%
$
'
Exception et ordre d’évaluation
exception Ex of int;;
let f x = raise (Ex x);;
type de f ????
Intéressons nous à l’expression (f 1) + (f 2) , bien sûr, elle n’a pas
de valeur
Mais quelle est l’exception levée par cette expression ?
⇒ On ne peut répondre à cette question qu’en connaissant
l’ordre d’évaluation des sous-expressions d’une addition
• Si évaluation de gauche à droite : échec avec l’exception Exc 1
• Si évaluation de droite à gauche : échec avec l’exception Exc 2
Aie, Aie, ..... Si par la suite on récupère l’exception, le résultat fourni
peut être différent ....
&
24
%
'
let g x = try (f x) + (f (x+1)) with
Ex n -> n;;
$
Et en OCaml, que se passe-t-il ?
(f 1) + (f 2);;
exception: Ex 2
donc
(g 1) vaut 2.
Autre casse-tête :
# let g x y = (x,y);;
val g : ’a -> ’b -> ’a * ’b = <fun>
# g 3 4;;
- : int * int = (3, 4)
# g (f 1) (f 2);;
?????
On constate qu’il est important ici de connaı̂tre l’ordre d’évaluation
d’une addition alors que ceci était complètement transparent tant que
l’on n’utilisait pas le mécanisme des exceptions.
&
25
%
$
'
A manipuler avec précaution !!!!
En effet, on ne saurait faire l’hypothèse d’un certain ordre d’évaluation
quand on programme car l’ordre d’évaluation n’est pas officiellement
spécifié en OCaml. Il pourrait changer par exemple d’une version du
compilateur à une autre !
Ne pas écrire de programmes qui dépendent de l’ordre d’évaluation : ce
sont des programmes de mauvaise qualité.
⇒ Avec les exceptions on sort du fonctionnel pur !!!
&
26
%
$
'
Exemple avancé : Un algorithme à essais successifs
Soit le problème suivant : on dispose d’une liste d’entiers, on désire en
extraire une sous-liste dont la somme des éléments fasse un certain
entier s.
Par exemple une sous-liste solution du problème posé avec la liste
initiale [1;2;-6;3;5] et la somme s égale à 10 est la liste [2; 3; 5].
&
27
%
$
'
Le principe de l’algorithme est très simple :
• si la liste est vide, alors si la somme est nulle la solution est la liste
vide sinon il n’y a pas de solution ;
• si la liste n’est pas vide, donc de la forme x::r, deux choix :
– essayer une solution composée de x et d’une sous-liste de la liste
r dont la somme des éléments fasse s - x
– ou répondre au problème initial en ne choisissant les éléments
de la solution que parmi les éléments de r.
&
28
%
'
La fonction suivante répond à cette approche en privilégiant le premier
choix que l’on essaye en priorité.
$
#exception Impossible;;
#let rec sous_liste s l = match l with
| [] -> if s = 0 then [] else raise Impossible
| x::r -> try x::(sous_liste (s-x) r) with
Impossible -> sous_liste s r;;
val sous_liste : int -> int list -> int list = <fun>
#sous_liste 10 [1;2;-6;3;5];;
- : int list = [2; 3; 5]
#sous_liste (-10) [16;2;(-6);3;5];;
Exception: Impossible.
#sous_liste 0 [5;-1;-5;-4;10];;
- : int list = [5; -1; -4]
&
29
%
$
'
Cet algorithme rentre dans la catégorie dite des algorithmes à essais
successifs encore appelés algorithmes à retour, en anglais backtrack. Ils
construisent progressivement une solution du problème en revenant en
arrière lors de la rencontre d’une impasse : on remet alors en cause le
dernier choix effectué (dans l’exemple, on renonce à incorporer x dans
le résultat).
&
30
%