web-dev-qa-db-fra.com

Comment gérer la division par zéro dans une langue qui ne prend pas en charge les exceptions?

Je suis en train de développer un nouveau langage de programmation pour résoudre certaines exigences commerciales, et ce langage est destiné aux utilisateurs novices. Il n'y a donc pas de support pour la gestion des exceptions dans le langage, et je ne m'attendrais pas à ce qu'ils l'utilisent même si je l'ai ajouté.

J'ai atteint le point où je dois implémenter l'opérateur de division, et je me demande comment gérer au mieux une division par zéro erreur?

Il me semble n'avoir que trois façons possibles de gérer cette affaire.

  1. Ignorez l'erreur et produisez 0 comme résultat. Enregistrer un avertissement si possible.
  2. Ajoutez NaN comme valeur possible pour les nombres, mais cela soulève des questions sur la façon de gérer les valeurs de NaN dans d'autres zones de la langue.
  3. Arrêtez l'exécution du programme et signalez à l'utilisateur qu'une erreur grave s'est produite.

L'option n ° 1 semble la seule solution raisonnable. L'option n ° 3 n'est pas pratique car ce langage sera utilisé pour exécuter la logique comme un cron nocturne.

Quelles sont mes alternatives à la gestion d'une division par zéro erreur, et quels sont les risques associés à l'option # 1.

62
Reactgular

Je déconseille fortement le n ° 1, car ignorer les erreurs est un anti-modèle dangereux. Cela peut conduire à des bogues difficiles à analyser. Définir le résultat d'une division par zéro à 0 n'a aucun sens, et continuer l'exécution du programme avec une valeur absurde va causer des problèmes. Surtout lorsque le programme fonctionne sans surveillance. Lorsque l'interpréteur de programme remarque qu'il y a une erreur dans le programme (et une division par zéro est presque toujours une erreur de conception), il est généralement préférable de l'interrompre et de tout garder tel quel plutôt que de remplir votre base de données avec des ordures.

De plus, il est peu probable que vous réussissiez à bien suivre ce modèle. Tôt ou tard, vous rencontrerez des situations d'erreur qui ne peuvent tout simplement pas être ignorées (comme un manque de mémoire ou un débordement de pile) et vous devrez de toute façon implémenter un moyen de terminer le programme.

L'option n ° 2 (en utilisant NaN) serait un peu de travail, mais pas autant que vous pourriez le penser. La façon de gérer NaN dans différents calculs est bien documentée dans la norme IEEE 754, vous pouvez donc probablement faire ce que fait la langue de votre interprète.

Au fait: Créer un langage de programmation utilisable par des non-programmeurs est quelque chose que nous essayons de faire depuis 1964 (Dartmouth BASIC). Jusqu'à présent, nous avons échoué. Mais bonne chance quand même.

98
Philipp

1 - Ignorez l'erreur et produisez 0 comme résultat. Enregistrer un avertissement si possible.

Ce n'est pas une bonne idée. Du tout. Les gens vont commencer en fonction de cela et si vous le corrigez, vous casserez beaucoup de code.

2 - Ajoutez NaN comme valeur possible pour les nombres, mais cela soulève des questions sur la façon de gérer les valeurs de NaN dans d'autres zones de la langue.

Vous devez gérer NaN comme le font les temps d'exécution d'autres langages: tout calcul supplémentaire donne également NaN et chaque comparaison (même NaN == NaN) donne faux.

Je pense que c'est acceptable, mais pas nécessairement nouveau pour les nouveaux arrivants.

3 - Mettre fin à l'exécution du programme et signaler à l'utilisateur une grave erreur.

C'est la meilleure solution, je pense. Avec ces informations en main, les utilisateurs devraient pouvoir gérer 0. Vous devez fournir un environnement de test, en particulier s'il est destiné à s'exécuter une fois par nuit.

Il existe également une quatrième option. Faites de la division une opération ternaire. Chacun de ces deux fonctionnera:

  • div (numérateur, dénumérateur, résultat_alternatif)
  • div (numérateur, dénumérateur, dénumérateur alternatif)
33
back2dos

Arrêtez l'application en cours avec un préjudice extrême. (Tout en fournissant des informations de débogage adéquates)

Apprenez ensuite à vos utilisateurs à identifier et à gérer les conditions dans lesquelles le diviseur peut être nul (valeurs saisies par l'utilisateur, etc.)

21
Dave Nay

Dans Haskell (et similaire dans Scala), au lieu de lever des exceptions (ou de renvoyer des références nulles), les types d'encapsuleur Maybe et Either peuvent être utilisés. Avec Maybe, l'utilisateur a la possibilité de tester si la valeur qu'il a obtenue est "vide", ou il peut fournir une valeur par défaut lors du "déballage". Either est similaire, mais peut être utilisé renvoie un objet (par exemple une chaîne d'erreur) décrivant le problème s'il y en a un.

13
Landei

D'autres réponses ont déjà considéré les mérites relatifs de vos idées. J'en propose une autre: utiliser l'analyse de flux de base pour déterminer si une variable peut être nulle. Ensuite, vous pouvez simplement interdire la division par des variables potentiellement nulles.

x = ...
y = ...

if y ≠ 0:
  return x / y    // In this block, y is known to be nonzero.
else:
  return x / y    // This, however, is a compile-time error.

Vous pouvez également avoir une fonction d'assertion intelligente qui établit des invariants:

x = ...
require x ≠ 0, "Unexpected zero in calculation"
// For the remainder of this scope, x is known to be nonzero.

Cela revient à lancer une erreur d'exécution - vous contournez entièrement les opérations non définies - mais présente l'avantage que le chemin du code n'a même pas besoin d'être atteint pour que l'échec potentiel soit exposé. Cela peut être fait un peu comme le contrôle de typage ordinaire, en évaluant toutes les branches d'un programme avec des environnements de typage imbriqués pour suivre et vérifier les invariants:

x = ...           // env1 = { x :: int }
y = ...           // env2 = env1 + { y :: int }
if y ≠ 0:         // env3 = env2 + { y ≠ 0 }
  return x / y    // (/) :: (int, int ≠ 0) → int
else:             // env4 = env2 + { y = 0 }
  ...
...               // env5 = env2

De plus, il s'étend naturellement à la plage et à la vérification de null, si votre langue possède de telles fonctionnalités.

12
Jon Purdy

Le numéro 1 (insérer un zéro non débogable) est toujours mauvais. Le choix entre # 2 (propager NaN) et # 3 (tuer le processus) dépend du contexte et devrait idéalement être un paramètre global, comme c'est le cas dans Numpy.

Si vous faites un grand calcul intégré, la propagation de NaN est une mauvaise idée car elle finira par se propager et infecter tout votre calcul --- lorsque vous regardez les résultats le matin et voyez qu'ils sont tous NaN, vous '' Je dois jeter les résultats et recommencer quand même. Il aurait été préférable que le programme se termine, vous receviez un appel au milieu de la nuit et le répariez --- en termes de nombre d'heures perdues, au moins.

Si vous effectuez de nombreux petits calculs principalement indépendants (comme des calculs de réduction de carte ou des calculs parallèles embarrassants), et que vous pouvez tolérer qu'un certain pourcentage d'entre eux soit inutilisable en raison des NaN, c'est probablement la meilleure option. Mettre fin au programme et ne pas faire les 99% qui seraient bons et utiles à cause des 1% malformés et divisés par zéro pourrait être une erreur.

Autre option, liée aux NaN: la même spécification en virgule flottante IEEE définit Inf et -Inf, et celles-ci sont propagées différemment de NaN. Par exemple, je suis presque sûr que Inf> n'importe quel nombre et -Inf <n'importe quel nombre, ce qui serait ce que vous vouliez si votre division par zéro se produisait parce que le zéro était juste censé être un petit nombre. Si vos entrées sont arrondies et souffrent d'une erreur de mesure (comme les mesures physiques prises à la main), la différence de deux grandes quantités peut entraîner zéro. Sans la division par zéro, vous auriez obtenu un grand nombre, et peut-être que vous ne vous souciez pas de sa taille. Dans ce cas, In et -Inf sont des résultats parfaitement valides.

Il peut également être formellement correct --- dites simplement que vous travaillez dans les réels étendus.

11
Jim Pivarski

. Arrêtez l'exécution du programme et signalez à l'utilisateur qu'une erreur grave s'est produite.

[Cette option] n'est pas pratique…

Bien sûr, c'est pratique: c'est la responsabilité des programmeurs d'écrire un programme qui a du sens. La division par 0 n'a aucun sens. Par conséquent, si le programmeur effectue une division, il est également de sa responsabilité de vérifier au préalable que le diviseur n'est pas égal à 0. Si le programmeur ne parvient pas à effectuer cette vérification de validation, alors il/elle devrait réaliser cette erreur dès que possible, et les résultats de calcul dénormalisés (NaN) ou incorrects (0) ne seront tout simplement pas utiles à cet égard.

L'option 3 se trouve être celle que je vous aurais recommandée, au fait, pour être la plus simple, honnête et mathématiquement correcte.

8
stakx

Cela me semble une mauvaise idée d'exécuter des tâches importantes (par exemple, "cron nocturne") dans un environnement où les erreurs sont ignorées. C'est une terrible idée d'en faire une fonctionnalité. Cela exclut les options 1 et 2.

L'option 3 est la seule solution acceptable. Les exceptions ne doivent pas nécessairement faire partie de la langue, mais elles font partie de la réalité. Votre message de résiliation doit être aussi précis et informatif que possible sur l'erreur.

4
ddyer

IEEE 754 a en fait une solution bien définie pour votre problème. Gestion des exceptions sans utiliser exceptionshttp://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling

1/0  = Inf
-1/0 = -Inf
0/0  = NaN

de cette façon, toutes vos opérations ont un sens mathématique.

\ lim_ {x\to 0} 1/x = Inf

À mon avis, suivre IEEE 754 est le plus logique car il garantit que vos calculs sont aussi corrects que sur un ordinateur et que vous êtes également cohérent avec le comportement des autres langages de programmation.

Le seul problème qui se pose est que Inf et NaN vont contaminer vos résultats et vos utilisateurs ne sauront pas exactement d'où vient le problème. Jetez un œil à une langue comme Julia qui le fait très bien.

Julia> 1/0
Inf

Julia> -1/0
-Inf

Julia> 0/0
NaN

Julia> a = [1,1,1] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 Inf

Julia> sum(a)
Inf

Julia> a = [1,1,0] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 NaN

Julia> sum(a)
NaN

L'erreur de division se propage correctement à travers les opérations mathématiques mais à la fin l'utilisateur ne sait pas nécessairement de quelle opération provient l'erreur.

edit: Je n'ai pas vu la deuxième partie de la réponse de Jim Pivarski qui est essentiellement ce que je dis ci-dessus. Ma faute.

3
wallnuss

SQL, facilement le langage le plus utilisé par les non-programmeurs, fait le # 3, pour tout ce qui en vaut la peine. D'après mon expérience en observant et en aidant des non-programmeurs à écrire du SQL, ce comportement est généralement bien compris et facilement compensé (avec une déclaration de cas ou similaire). Il est utile que le message d'erreur que vous obtenez ait tendance à être assez direct, par exemple dans Postgres 9, vous obtenez "ERREUR: division par zéro".

2
Noah Yetter

Je pense que le problème est "destiné aux utilisateurs novices. -> Il n'y a donc pas de support pour ..."

Pourquoi pensez-vous que la gestion des exceptions est problématique pour les utilisateurs novices?

Qu'est-ce qui est pire? Vous avez une fonction "difficile" ou vous ne savez pas pourquoi quelque chose s'est produit? Quoi de plus confus? Un crash avec un core dump ou "Fatal error: Divide by Zero"?

Au lieu de cela, je pense qu'il vaut bien mieux viser de GRANDES erreurs de message. Faites donc plutôt: "Mauvais calcul, divisez 0/0" (c'est-à-dire: affichez toujours les DONNÉES qui causent le problème, pas seulement le type du problème) . Regardez comment PostgreSql fait les erreurs de message, c'est génial à mon humble avis.

Cependant, vous pouvez examiner d'autres façons de travailler avec des exceptions comme:

http://dlang.org/exception-safe.html

J'ai également rêvé de construire un langage, et dans ce cas, je pense que mélanger un peut-être/facultatif avec des exceptions normales pourrait être le meilleur:

def openFile(fileName): File | Exception
    if not(File.Exist(fileName)):
        raise FileNotExist(fileName)
    else:
        return File.Open()

#This cause a exception:

theFile = openFile('not exist')

# But this, not:

theFile | err = openFile('not exist')
2
mamcx

À mon avis, votre langage devrait fournir un mécanisme générique pour détecter et gérer les erreurs. Les erreurs de programmation devraient être détectées au moment de la compilation (ou le plus tôt possible) et devraient normalement conduire à l'arrêt du programme. Les erreurs qui résultent de données inattendues ou erronées, ou de conditions externes inattendues, doivent être détectées et mises à disposition pour une action appropriée, mais permettre au programme de continuer autant que possible.

Les actions plausibles incluent (a) terminer (b) Demander à l'utilisateur une action (c) enregistrer l'erreur (d) substituer une valeur corrigée (e) définir un indicateur à tester dans le code (f) invoquer une routine de gestion des erreurs. Lequel de ceux-ci vous mettez à disposition et par quels moyens avez-vous des choix à faire.

D'après mon expérience, les erreurs de données courantes telles que les conversions défectueuses, la division par zéro, le débordement et la valeur hors plage sont bénignes et devraient être traitées par défaut en remplaçant une valeur différente et en définissant un indicateur d'erreur. Le (non programmeur) utilisant ce langage verra les données erronées et comprendra rapidement la nécessité de vérifier les erreurs et de les traiter.

[Pour un exemple, considérez une feuille de calcul Excel. Excel ne met pas fin à votre feuille de calcul car un nombre a débordé ou autre. La cellule obtient une valeur étrange et vous allez découvrir pourquoi et la réparer.]

Donc, pour répondre à votre question: vous ne devez certainement pas terminer. Vous pouvez remplacer NaN mais vous ne devez pas le rendre visible, assurez-vous simplement que le calcul se termine et génère une valeur élevée étrange. Et définissez un indicateur d'erreur afin que les utilisateurs qui en ont besoin puissent déterminer qu'une erreur s'est produite.

Divulgation: J'ai créé une telle implémentation de langage (Powerflex) et j'ai traité exactement ce problème (et bien d'autres) dans les années 1980. Il y a eu peu ou pas de progrès sur les langages pour les non-programmeurs au cours des 20 dernières années environ, et vous attirerez des tas de critiques pour avoir essayé, mais j'espère vraiment que vous réussirez.

1
david.pfx

J'ai aimé l'opérateur ternaire où vous fournissez une valeur alternative au cas où le dénumérateur est 0.

Une autre idée que je n'ai pas vue est de produire une valeur générale "invalide". Une variable générale "cette variable n'a pas de valeur car le programme a fait quelque chose de mal", qui porte une trace de pile complète avec elle-même. Ensuite, si vous utilisez cette valeur n'importe où, le résultat est à nouveau invalide, avec la nouvelle opération tentée en haut (c.-à-d. Si la valeur invalide apparaît jamais dans une expression, l'expression entière renvoie invalide et aucun appel de fonction n'est tenté; une exception serait être des opérateurs booléens - vrai ou invalide est vrai et faux et invalide est faux - il peut aussi y avoir d'autres exceptions). Une fois que cette valeur n'est plus référencée nulle part, vous enregistrez une longue description de Nice de toute la chaîne où les choses ont mal tourné et continuez comme d'habitude. Peut-être envoyer la trace au responsable du projet ou quelque chose.

Quelque chose comme la monade peut-être au fond. Cela fonctionnera avec tout ce qui peut également échouer, et vous pouvez permettre aux gens de construire leurs propres invalides. Et le programme continuera de fonctionner tant que l'erreur n'est pas trop profonde, ce qui est vraiment souhaité ici, je pense.

1
Moshev

Il y a deux raisons fondamentales pour diviser par zéro.

  1. Dans un modèle précis (comme les entiers), vous obtenez une division par zéro DBZ car l'entrée est incorrecte. C'est le genre de DBZ auquel la plupart d'entre nous pensent.
  2. Dans un modèle non précis (comme flottant pt), vous pouvez obtenir un DBZ en raison d'une erreur d'arrondi même si l'entrée est valide. C'est ce à quoi nous ne pensons pas normalement.

Pour 1. vous devez communiquer aux utilisateurs qu'ils ont fait une erreur car ils sont les seuls responsables et ce sont eux qui savent le mieux comment remédier à la situation.

Pour 2. Ce n'est pas la faute de l'utilisateur, vous pouvez pointer du doigt l'algorithme, l'implémentation matérielle, etc. mais ce n'est pas la faute de l'utilisateur, vous ne devez donc pas terminer le programme ni même lever d'exception (si cela est autorisé, ce qui n'est pas le cas). Une solution raisonnable consiste donc à poursuivre les opérations d'une manière raisonnable.

Je peux voir que la personne posant cette question a demandé le cas 1. Vous devez donc communiquer à nouveau avec l'utilisateur. En utilisant n'importe quel standard à virgule flottante, Inf, -Inf, Nan, IEEE ne convient pas dans cette situation. Stratégie fondamentalement erronée.

1
InformedA

Lorsque vous écrivez un langage de programmation, vous devez en profiter et rendre obligatoire l'inclusion d'une action pour le dispositif par état zéro. a <= n/c: 0 action div-by-zero

Je sais que ce que je viens de suggérer est essentiellement d'ajouter un "goto" à votre PL.

0
Stephen

Interdisez-le dans la langue. C'est-à-dire, interdire la division par un nombre jusqu'à ce qu'il ne soit pas prouvé nul, généralement en le testant d'abord. C'est à dire.

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero
0
MSalters