Les erreurs et les interruptions.
Préparez-vous à vous opposer à la loi de Murphy, car soyez sûr que, si un événement hautement improbable peut arriver, il arrivera.
On peut penser, par exemple, à l'accès à un fichier qui n'existe pas sur le disque:
- s'il fait partie du programme, c'est un échec,
- s'il est demandé par l'utilisateur, c'est une autre situation exceptionnelle qu'il faut traiter de façon ad hoc (ce n'est pas un échec).
ou encore à l'accès au disque alors qu'il est plein: il y a épuisement d'une ressource.
ou encore à l'accès au serveur alors qu'il tombe en panne.
Une grande caractéristique de LISP est son système de "condition", plus flexible que les systèmes de traitement d'exception d'autres langages.
Cela va au delà du traitement des erreurs: on peut utiliser des conditions pour émettre un message d'alerte sans arrêter le programme.
Pour affiner ces notions, voyez:
http://dept-info.labri.u-bordeaux.fr/~idurand/enseignement/PFS/Book/node187.html
ou encore (en anglais):
http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html
ASSERT: (macro)
(defun imp (nb)
(assert (numberp nb))
(print nb))
(imp 45)
45
45 NB étant un nombre, il est affiché
(imp 'a)
(NB n'est pas un nombre: ouverture du débogueur)
The assertion (NUMBERP NB) failed with NB = A.
[Condition of type SIMPLE-ERROR]
SIGNAL: pour signaler une condition.
(defun imp (nb)
(if (numberp nb)
(print nb)
(signal 'error)))
(imp 56)
56
56 pas d'erreur
(imp 'a)
NIL rend NIL si la condition n'est pas traitée
HANDLER-CASE: (macro) Pour traiter les conditions.
(handler-case (imp 'a)
(condition () "Il y a un hic."))
"Il y a un hic."
ERROR: provoque une interruption de la procédure, ouvre le débogueur et affiche le message d'erreur.
le message d'erreur se construit comme avec FORMAT.
(defun imprime (nb)
(if (numberp nb)
(print nb)
(error "Cette valeur ~S n'est pas un nombre." nb)))
IMPRIME
(imprime 45)
45
45
(imprime 'a)
Cette valeur A n'est pas un nombre.
[Condition of type SIMPLE-ERROR]
...etc, et en sortant du débogueur:
; Evaluation aborted on #
dans le débogueur ~S est remplacé par la valeur de nb.
IGNORE-ERRORS: permet d'indiquer une erreur sans ouvrir le débogueur.
(ignore-errors (imprime 'a))
NIL
#
rend deux valeurs si une condition de type error est signalée: NIL et la condition signalée
Remarque: l'utilisation excessive de IGNORE-ERRORS ne permet pas d'améliorer la fiabilité d'un programme, puisque le débogueur n'est pas ouvert.
A la place de IGNORE-ERRORS, on aurait pu écrire:
(handler-case (imprime 'a)
(error (condition)
(values nil condition)))
NIL
#
on obtient le même résultat
l'intérêt est qu'on peut spécifier un type de condition particulier plutôt que "error"
Cependant on peut vouloir absolument ignorer une erreur.
Rappeler-vous le prédicat o-ou-n-p, quand on tapait une virgule en guise de réponse on obtenait une erreur du reader:
#<SB-INT:SIMPLE-READER-ERROR "Comma not inside a backquote." {1002C68A63}>
Voici le prédicat corrigé:
(defun o-ou-n-p (question)
"Affiche une question dont la réponse sera oui ou non, et attend une réponse:
rend T si la réponse est affirmative, NIL sinon."
(setq question (concatenate 'string question " ")) ;pour la présentation de l'affichage
(format t "~a" question) ;affichage de la question
(let ((rép (ignore-errors (read)))) ;attente de la réponse
(case rép ;traitement de la réponse
((o oui yes y ja) t)
((n non no nein) nil)
(t (princ "Répondez par oui ou non svp. ") (o-ou-n-p question))))) ;cas d'une réponse inattendue
Notez que la fonction ignore-errors s'applique à (read) uniquement (et non au LET).
En effet, il s'agit d'une erreur du reader.
CERROR: permet de proposer dans le débogueur un autre calcul plus correct quand une erreur est signalée.
(defun factorielle (x)
(cond ((not (typep x 'integer))
(error "~S n'est pas un entier." x))
((minusp x)
(let ((opp-x (- x)))
(cerror "Calculez plutôt: -(~D!)."
"La factorielle d'un entier négatif: (-~D)! n'a pas de sens." opp-x)
(format t "- factorielle de ~D: " opp-x)
(- (factorielle opp-x))))
((zerop x) 1)
(t (* x (factorielle (- x 1))))))
(factorielle -4)
- factorielle de 4:
-24 le calcul de (-4)! est transformé en -4! après passage par le débogueur
Autre exemple:
(defun divise (dividende diviseur)
(cond ((or (not (numberp dividende)) (not (numberp diviseur)))
(error "(Parmi les arguments '~S '~S) au moins un ne convient pas à la fonction DIVISE." dividende diviseur))
((zerop diviseur) (error 'division-by-zero :operator 'divise :operands (list dividende diviseur)))
(t (/ dividende diviseur))))
(divise 'a 5)
ouverture du débogueur:
(Parmi les arguments 'A '5) au moins un ne convient pas à la fonction DIVISE.
[Condition of type SIMPLE-ERROR]
ouverture du débogueur:
arithmetic error DIVISION-BY-ZERO signalled
[Condition of type DIVISION-BY-ZERO]
Et, avec HANDLER-CASE:
(handler-case (divise 5 'a)
(simple-error (condition)
(values nil condition))
(arithmetic-error (condition)
(values nil condition)))
NIL
#
(handler-case (divise 5 0)
(simple-error (condition)
(values nil condition))
(arithmetic-error (condition)
(values nil condition)))
NIL
#
CATCH:
THROW:
(defun inverse (lst)
"rend la liste des inverses."
(catch 'arrêt
(cond
((endp lst) nil)
((equal (car lst) 0) (throw 'arrêt "division par zéro interdite."))
(t (cons (/ 1 (car lst)) (inverse (cdr lst)))))
))
INVERSE définit une fonction donnant la liste des inverses des nombres de la liste passée en argument
avec interruption si le nombre est égal à zéro
(inverse '(1 2 3 4 5))
(1 1/2 1/3 1/4 1/5) la procédure s'est effectuée normalement.
(inverse '(1 2 3 4 0 5))
(1 1/2 1/3 1/4 . "division par zéro interdite.") interruption avec un message d'erreur.
A la place d'une interruption et d'un message d'erreur, on peut demander un traitement particulier de la situation:
(defun inverse (lst)
"rend la liste des inverses."
(catch 'arrêt
(cond
((endp lst) nil)
((equal (car lst) 0) (throw 'arrêt (cons "1/0" (inverse (cdr lst)))))
(t (cons (/ 1 (car lst)) (inverse (cdr lst)))))))
STYLE-WARNING: redefining COMMON-LISP-USER::INVERSE in DEFUN
INVERSE la définition de inverse a changé au niveau du THROW
(inverse '(1 2 3 0 4 5))
(1 1/2 1/3 "1/0" 1/4 1/5) on a un résultat insolite dans la liste, mais la récursion se poursuit.
Remarque: THROW peut être placé dans une autre fonction qui traitera les erreurs:
(defun traitement ()
(throw 'arrêt "division par zéro interdite."))
(defun inverse (lst)
"rend la liste des inverses."
(catch 'arrêt
(cond
((endp lst) nil)
((equal (car lst) 0) (traitement))
(t (cons (/ 1 (car lst)) (inverse (cdr lst)))))
))
(inverse '(1 2 3 4 0 5))
(1 1/2 1/3 1/4 . "division par zéro interdite.")
Voir aussi BLOCK: et RETURN-FROM:.
A la différence de CATCH-THROW, RETURN-FROM doit être dans le BLOCK.
La prochaine fois: FONCTIONS et FERMETURES.