Typage, domaine de définition et exceptions
Le type inféré d'une fonction correspond à un sur-ensemble de son
domaine de définition. Ce n'est pas parce qu'une fonction prend un
paramètre de type int qu'elle saura calculer une valeur pour
tous les entiers passés en argument. On traite en général ce
problème en utilisant le mécanisme d'exceptions d'Objective CAML.
Le déclenchement d'une exception provoque une rupture du calcul
qui peut être interceptée et traitée par le programme. Pour cela
l'exécution du programme doit avoir posé un récupérateur d'exception
avant le calcul de l'expression qui provoque le déclenchement de
cette exception.
Fonctions partielles et exceptions
Le domaine de définition d'une fonction correspond à l'ensemble des
valeurs sur lesquelles la fonction effectue son calcul. Il existe de
nombreuses fonctions mathématiques partielles, on peut citer la
division ou le logarithme népérien. Ce problème se pose aussi
pour les fonctions manipulant des structures de données plus complexes.
En effet quel est le résultat du calcul du premier élément
d'une liste vide ? De la même manière le calcul de la fonction
factorielle sur un entier négatif peut entraîner un calcul
infini.
Un certain nombre de situations exceptionnelles peuvent se produire
durant l'exécution d'un programme, par exemple une tentative de
division par zéro. Tenter de diviser un nombre par zéro provoquera
au mieux l'arrêt du programme, au pire un état incohérent de la
machine. La sûreté d'un langage de programmation passe
par la garantie de ne pas se retrouver dans une telle situation
pour ces cas particuliers. Les exceptions sont une manière d'y
répondre.
La division de 1
par 0
provoquera le déclenchement
d'une exception spécifique :
# 1
/
0
;;
Uncaught exception: Division_by_zero
Le message Uncaught exception: Division_by_zero
indique d'une
part que l'exception Division_by_zero a été déclenchée, et que
d'autre part elle n'a pas été récupérée. Cette exception
fait partie des déclarations de base du langage.
Souvent, le type d'une fonction ne correspond pas à son
domaine de définition quand un filtrage de motif n'est pas exhaustif,
c'est à dire qu'il ne filtre pas tous les cas de l'expression
donnée. Pour prévenir une telle erreur, Objective CAML affiche un message
dans un tel cas.
# let
tete
l
=
match
l
with
t::q
->
t
;;
Characters 14-36:
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
val tete : 'a list -> 'a = <fun>
Si néanmoins le programmeur maintient sa définition incomplète,
Objective CAML utilisera le mécanisme d'exceptions en cas d'appel erroné
à la fonction partielle :
# tete
[]
;;
Uncaught exception: Match_failure("", 14, 36)
Enfin, nous avons déjà rencontré une autre exception prédéfinie :
Failure. Elle prend un argument de type string.
On peut déclencher cette exception en utilisant la fonction
failwith. On pourra ainsi l'utiliser pour compléter notre définition de la
fonction tete :
# let
tete
=
function
[]
->
failwith
"Liste vide"
|
h::t
->
h;;
val tete : 'a list -> 'a = <fun>
# tete
[]
;;
Uncaught exception: Failure("Liste vide")
Définition d'une exception
En Objective CAML, les exceptions appartiennent à un type prédéfini
exn. Ce type est très particulier puisque c'est un type
somme extensible : on peut étendre l'ensemble des valeurs du
type en déclarant de nouveaux constructeurs. Cette particularité
permet à l'utilisateur de définir ses propres exceptions en
ajoutant au type exn des nouveaux constructeurs.
La syntaxe de la déclaration d'une exception est la suivante :
Syntaxe
exception Nom ;;
ou
Syntaxe
exception Nom of t ;;
Voici des exemples de déclarations d'exceptions :
# exception
A_MOI;;
exception A_MOI
# A_MOI;;
- : exn = A_MOI
# exception
Depth
of
int;;
exception Depth of int
# Depth
4
;;
- : exn = Depth(4)
Une exception est donc une valeur du langage à part entière.
Warning
Les noms d'exceptions sont des constructeurs. Ils commencent donc
obligatoirement par une majuscule.
# exception
minuscule
;;
Characters 11-20:
Syntax error
Warning
Les exceptions sont monomorphes : elles n'ont pas de
paramètre de type dans la déclaration du type de leur argument.
# exception
Value
of
'a
;;
Characters 20-22:
Unbound type parameter 'a
Une exception polymorphe autoriserait la définition de fonctions avec
un type de retour quelconque comme nous le verrons plus loin, page
??.
Déclenchement d'une exception
La fonction raise est une fonction primitive du langage. Elle
prend une exception comme argument et possède un type de retour
entièrement polymorphe.
# raise
;;
- : exn -> 'a = <fun>
# raise
A_MOI;;
Uncaught exception: A_MOI
# 1
+
(raise
A_MOI);;
Uncaught exception: A_MOI
# raise
(Depth
4
);;
Uncaught exception: Depth(4)
Il n'est pas possible d'écrire en Objective CAML la fonction raise. Elle doit
être prédéfinie.
Récupération d'une exception
Tout l'intérêt de déclencher des exceptions réside dans la
capacité de les récupérer et d'orienter la suite du calcul selon
la valeur de l'exception déclenchée. L'ordre de calcul d'une expression
prend alors de l'importance pour déterminer
quelle exception est déclenchée. On sort du cadre purement fonctionnel, où l'ordre
d'évaluation des arguments peut changer le résultat d'un calcul
comme il sera discuté au chapitre suivant (voir page
??).
La construction syntaxique suivante, qui calcule la valeur d'une expression,
permet la
récupération d'une exception déclenchée lors de ce calcul :
Syntaxe
try expr with |
| p1 -> expr1 |
: |
| pn -> exprn |
Si le calcul de expr ne déclenche pas d'exception, alors le
résultat est celui du calcul de expr. Sinon, la valeur de
l'exception déclenchée est filtrée; la valeur de l'expression
correspondante à la première branche du filtrage correct est
retournée. Si aucune branche du filtrage ne correspond à la valeur
de l'exception alors celle-ci se propage jusqu'au précédent
try-with posé durant l'exécution du programme.
Donc le filtrage d'une exception est toujours considéré comme exhaustif.
Implicitement, le dernier filtre est | e -> raise e .
Si
aucun récupérateur d'exception n'est rencontré dans le
programme, le système lui-même se charge d'intercepter
l'exception et termine le programme en affichant un message d'erreur.
Il ne faut pas confondre calculer une exception (c'est-à-dire
une valeur de type exn) et déclencher une exception qui
effectue une rupture de calcul. Une exception étant une valeur comme
les autres elle peut être rendue comme résultat d'une fonction.
# let
rendre
x
=
Failure
x
;;
val rendre : string -> exn = <fun>
# rendre
"test"
;;
- : exn = Failure("test")
# let
declencher
x
=
raise
(Failure
x)
;;
val declencher : string -> 'a = <fun>
# declencher
"test"
;;
Uncaught exception: Failure("test")
On remarque que l'application de declencher ne rend pas de valeur
alors que celle de rendre en rend une de type exn.
Calculer avec des exceptions
Outre leur utilisation pour le traitement de valeurs non désirées,
les exceptions permettent aussi un style de programmation et peuvent être
source d'optimisations. L'exemple suivant effectue le
produit de tous les éléments d'une liste d'entiers. On utilise une
exception pour interrompre le parcours de la liste et retourner la
valeur 0 dès qu'on la rencontre.
# exception
Found_zero
;;
exception Found_zero
# let
rec
mult_rec
l
=
match
l
with
[]
->
1
|
0
::
_
->
raise
Found_zero
|
n
::
x
->
n
*
(mult_rec
x)
;;
val mult_rec : int list -> int = <fun>
# let
mult_list
l
=
try
mult_rec
l
with
Found_zero
->
0
;;
val mult_list : int list -> int = <fun>
# mult_list
[
1
;2
;3
;0
;5
;6
]
;;
- : int = 0
Ainsi tous les calculs restant en attente,
à savoir les multiplications par n qui suivent chacun des appels
récursifs, sont abandonnées. Après avoir rencontré le
raise, le calcul reprend à partir du filtrage du
with.