Précédent Index Suivant

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.


Précédent Index Suivant