Yliur <
yl...@free.fr> writes:
> Bonjour
>
> J'ai un traitement qui peut planter et je voudrais :
> - Afficher un message d'erreur gentil à l'utilisateur.
> - Éviter que la fonction soit interrompue (par exemple parce
> qu'elle a autre chose à faire après).
Lire:
http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html
http://www.nhplace.com/kent/Papers/Condition-Handling-2001.html
http://chaitanyagupta.com/lisp/restarts.html
et bien sur le chapitre 9 de CLHS.
> Pour l'instant j'ai écrit quelque chose comme ça :
>
> (handler-case
> (là des trucs à faire...)
> (serious-condition (e)
> (format t "Erreur blablabla")
> (print e)))
>
> Les erreurs assez grave pour interrompre l'exécution devraient être
> capturées par la clause serious-condition (e)
> (du moins si ces erreurs héritent bien de serious-condition).
Une SERIOUS-CONDITION, ce n'est pas une erreur!
> Et d'éventuelles conditions moins graves (en fait, n'héritant pas de
> serious-condition, des avertissements par exemple) ne seraient pas
> concernés, ce qui évite de tout casser si une fonction signale un simple
> avertissement.
Signaler n'importe quelle condition qui n'est pas traitée ne casse rien,
l'exécution continue:
(handler-case
(progn (signal 'division-by-zero)
42)
(file-error () 'ha!))
--> 42
(handler-case
(progn (signal 'division-by-zero)
42)
(division-by-zero () 'ha!))
--> HA!
> Protection du code :
> - Je n'ai pas dit de bêtises ?
> - Le code ne me paraît pas très bien protégé : comment éviter
> qu'une condition qui n'hériterait pas de serious-condition ne
> traverse ?
On n'évite pas. Il ne faut pas éviter. Les conditions ne sont pas
toutes des erreurs. Il vaut mieux attraper au maximum les ERREUR, et
pas les SERIOUS-CONDITION. Par exemple, il est possible que lorsqu'il
n'y a plus de mémoire, une SERIOUS-CONDITION soit signalée, et attrapée
par une boucle de haut niveau, qui ferait appel au ramasse-miette. Si
tu t'intercale là, tu pourrais avoir des problèmes.
De toutes façons, une SERIOUS-CONDITION, ce n'est pas une erreur!
> Ou encore des branchements non dus à des conditions ?
> Il y a bien unwind-protect qui permet de tout attraper, mais une
> fois son "nettoyage" terminé le code ne continue pas à la suite du
> bloc unwind-protect.
Il faut le dire. Comment veux tu faire la différence entre:
- tout attraper et arrêter
- tout attraper et continuer
?
Et d'ailleurs, continuer où? On ne peut pas deviner, il faut que tu le
programme!
(loop
with done = nil
do (unwind-protect
(progn
(do-something)
(setf done t))
(clean-something))
until done)
> Informations sur l'erreur :
> - Pour les erreurs standards de Common Lisp, y a-t-il un moyen de
> récupérer à l'intérieur de l'objet représentant la condition un
> texte un peu moins moche que celui obtenu par print par exemple
> ("#<SYSTEM::SIMPLE-DIVISION-BY-ZERO#x21B02226>") ?
Utiliser ~A au lieu de ~S, princ au lieu de prin1.
> Le débogueur
> affiche un message un peu plus joli, mais je ne sais pas s'il le
> tire de l'objet lui-même ou si c'est lui qui le produit (il l'a
> écrit en français, donc sans doute l'option 2).
Regarder la documentation de la condition normalisé (ou spécifique à
l'implémentation). Ne pas oublier les super-classes!
Par exemple, DIVISION-BY-ZERO est une ARITHMETIC-ERROR, et une
ARITHMETIC-ERROR a deux attributs:
ARITHMETIC-ERROR-OPERATION et ARITHMETIC-ERROR-OPERANDS.
Cependant, l'erreur peut survenir dans des couches internes ou après
moultes optiomisation, alors ces attributs ne sont pas forcément en
relation avec les arguments et opérateurs donnés dans le programme:
CL-USER> (handler-case
(if (zerop (random 2))
(/ 0)
(/ 3 2 1 0))
(division-by-zero (err)
(format t "division by zero, operation: ~S, operands: ~S~%"
(arithmetic-error-operation err)
(arithmetic-error-operands err))))
division by zero, operation: /, operands: (3/2 0) ; on peut supposer la deuxième branche.
NIL
CL-USER> (handler-case
(if (zerop (random 2))
(/ 0)
(/ 3 2 1 0))
(division-by-zero (err)
(format t "division by zero, operation: ~S, operands: ~S~%"
(arithmetic-error-operation err)
(arithmetic-error-operands err))))
division by zero, operation: CCL::UNKNOWN, operands: NIL ; pas d'information,
NIL ; l'erreur est probablement optimisée directement dans la
; première branche.
> - Y a-t-il un moyen de récupérer une pile d'appels, pas dans le
> débogueur mais dans le programme, sous forme de chaîne ou de
> liste, ... quelque chose qui pourrait par exemple être stocké
> dans un journal ?
C'est possible, mais en utilisant des fonctions spécifiques à chaque
implémentation. Voir les sources de swank (slime), il y aurait peut
être moyen d'en extraire une bibliothèque de portabilité.
Finalement, bien que le standard donne quelques indications sur les
conditions qui peuvent ou doivent être signalées par les operateurs
standardisés, il y a beaucoup de flou, et peu de restart sont
standardisés.
Par exemple, il n'est pas dit que dans le cas d'un appel à cl:/ tel que:
(cl:/ 1 2 3 0 4 5 6)
on ait des restarts tels que:
IGNORE-ZERO
GIVE-NEW-VALUE-FOR-ARGUMENT
GIVE-DIVISION-RESULT
(notament, car le standard veut laisser des possibilités
d'optimisation; cl:/ peut changer l'ordre des diviseurs, peut calculer
la division à la compilation si les operandes sont des constantes, etc).
Donc on obtient des listes de restarts complètement divergentes:
[pjb@kuiper :0 ~]$ clall -r '(block :divide (handler-bind ((division-by-zero (lambda (err) (return-from :divide (mapcar (function restart-name) (compute-restarts err)))))) (/ 1 2 3 0 4 5 6)))'
International Allegro CL Free Express Edition --> (EXCL::RETRY EXCL::SKIP EXCL::RECOMPILE-DUE-TO-INCOMPATIBLE-FASL ABORT)
Clozure Common Lisp --> (CCL::RETRY-LOAD CCL::SKIP-LOAD CCL::LOAD-OTHER CONTINUE ABORT ABORT-BREAK ABORT)
CLISP --> (SYSTEM::SKIP RETRY SYSTEM::STOP)
CMU Common Lisp --> (CONTINUE ABORT)
ECL --> (CONTINUE ABORT)
SBCL --> (CONTINUE ABORT ABORT QUIT)
========================================================================
On ne peut pas traiter les erreurs automatiquement directement au niveau
des opérateurs standardisés. Il faut le faire dans ses propres
fonctions. On peut écrire des emballages pour le faire. Par exemple:
(defun my-/ (numerator &rest denominators)
(let ((division-result))
(restart-case
(loop
:with new-denominator
:for denominator :in denominators
:for result = numerator
:then (restart-case
(/ result denominator)
(ignore-zero ()
:report (lambda (stream) (format stream "Ignore zero denominator"))
result)
(give-new-value-for-argument (new-denominator)
:report (lambda (stream) (format stream "Give new value for zero denominator"))
:interactive (lambda ()
(format *query-io* "Enter the new denominator: ")
(finish-output *query-io*)
(list (read *query-io*)))
(/ result new-denominator)))
:finally (return result))
(give-division-result (division-result)
:report (lambda (stream) (format stream "Give division result"))
:interactive (lambda ()
(format *query-io* "Enter the division result: ")
(finish-output *query-io*)
(list (read *query-io*)))
division-result))))
On peut alors traiter les erreurs de division par zéro en invoquant le
restart que l'on veut, ce qui fera faire à l'emballage le traitement
prévu dans ces différents cas:
(handler-bind
((division-by-zero (lambda (err)
(declare (ignore err))
(invoke-restart (find-restart 'ignore-zero)))))
(my-/ 1 2 3 0 4 5 6))
--> 1/360
(handler-bind
((division-by-zero (lambda (err)
(declare (ignore err))
(invoke-restart (find-restart 'give-new-value-for-argument) 1/1000000))))
(my-/ 1 2 3 0 4 5 6))
--> 25000/9
(handler-bind
((division-by-zero (lambda (err)
(declare (ignore err))
(invoke-restart (find-restart 'give-division-result) 'infinity))))
(my-/ 1 2 3 0 4 5 6))
--> INFINITY
Encore une fois, le standard ne spécifie pas de cas de restart pour les
operateurs standard en général (les conditions et restarts sont un ajout
tardif), d'où la nécessité de ces emballages, ou en pratique, de gérer
les erreurs au niveau du code utilisateur, pas au niveau des operateurs
standards.
Si une fonction n'est pas conçue pour être restartable, elle ne peut
pas l'être.
On pourrait définir un paquetage exportant des emballages pour tous les
operateurs standard, et l'utiliser au lieu de CL, mais ça ne rendrait
pas les fonctions les utilisant restartable, seulement, les emballages.
La difficulté, c'est de décider quel restart et quelle valeurs
utiliser. Si une fonction fait une division par zéro, le restart à
choisir, ou le résultat à retourner, dépend de la fonction. Sans la
connaitre, on ne peut pas choisir. Veut on substituer 1 pour 0? Veut
on retourner un grand nombre? Un symbole INFINITY?
Finalement, il y a aussi: IGNORE-ERRORS ;-)
--
__Pascal Bourguignon__
http://www.informatimago.com/
A bad day in () is better than a good day in {}.