¿Cómo manejar la división entre cero en un idioma que no admite excepciones?

62

Estoy a punto de desarrollar un nuevo lenguaje de programación para resolver algunos requisitos comerciales, y este lenguaje está dirigido a usuarios novatos. Por lo tanto, no hay soporte para el manejo de excepciones en el lenguaje, y no esperaría que lo usen incluso si lo agrego.

He llegado al punto en el que tengo que implementar el operador de división, y me pregunto cómo manejar mejor un error de división por cero.

Parece que solo tengo tres formas posibles de manejar este caso.

  1. Ignorar el error y producir 0como resultado. Registrando una advertencia si es posible.
  2. Agregue NaNcomo un posible valor para los números, pero eso genera preguntas sobre cómo manejar los NaNvalores en otras áreas del lenguaje.
  3. Termine la ejecución del programa e informe al usuario que se produjo un error grave.

La opción n. ° 1 parece la única solución razonable. La opción # 3 no es práctica ya que este lenguaje se usará para ejecutar la lógica como un cron nocturno.

¿Cuáles son mis alternativas para manejar un error de división por cero y cuáles son los riesgos de ir con la opción # 1?

Reactgular
fuente
12
si agregaste soporte de excepción y el usuario no lo captó, entonces tendrías la opción n. ° 3
Ratchet Freak
82
Tengo curiosidad, ¿qué tipo de requisito estúpido requeriría que crees un lenguaje de programación completamente nuevo? En mi experiencia, todos los idiomas jamás creado chupa (en el diseño o en la ejecución, a menudo en ambos) y se tomó sin razón mucho esfuerzo para conseguir incluso que mucho. Hay algunas excepciones al primero, pero no al segundo, y como son fácilmente <0.01% de los casos, probablemente son errores de medición ;-)
16
La mayoría de los nuevos idiomas de @delnan se crean para permitir que las reglas de negocio se separen de cómo se implementan. El usuario no necesita saber cómo reject "Foo"se implementó, sino simplemente que rechaza un documento si contiene la palabra clave Foo. Trato de hacer que el idioma sea fácil de leer usando términos con los que el usuario esté familiarizado. Proporcionar a los usuarios su propio lenguaje de programación les permite agregar reglas comerciales sin depender del personal técnico.
Reactgular
19
@Mathew Foscarini. Nunca, nunca, ignore el error y devuelva silenciosamente 0. Al hacer una división, 0 puede ser un valor perfectamente legal (por alguna razón, existe tal cosa en Power Basic, y es realmente un dolor). Si divide números de coma flotante, Nan o Inf sería bueno (eche un vistazo a IEEE 754 para comprender por qué). Si divide números enteros, puede detener el programa, nunca se debe permitir dividir por 0 (bueno, a menos que quiera implementar un verdadero sistema de excepción).
16
Me divierte y me fascina un dominio de negocio lo suficientemente complejo como para justificar un lenguaje de programación patentado y completo de Turing, pero lo suficientemente laxo como para tolerar resultados drásticamente inexactos.
Mark E. Haase

Respuestas:

98

Recomiendo encarecidamente contra el n. ° 1, porque ignorar los errores es un antipatrón peligroso. Puede conducir a errores difíciles de analizar. Establecer el resultado de una división por cero a 0 no tiene ningún sentido, y continuar la ejecución del programa con un valor sin sentido va a causar problemas. Especialmente cuando el programa se ejecuta sin supervisión. Cuando el intérprete del programa se da cuenta de que hay un error en el programa (y una división por cero es casi siempre un error de diseño), generalmente se prefiere abortarlo y mantener todo tal como está en lugar de llenar su base de datos con basura.

Además, es poco probable que tenga éxito si sigue completamente este patrón. Tarde o temprano, se encontrará con situaciones de error que simplemente no se pueden ignorar (como quedarse sin memoria o un desbordamiento de pila) y tendrá que implementar una forma de terminar el programa de todos modos.

La opción # 2 (usando NaN) sería un poco de trabajo, pero no tanto como podría pensar. La forma de manejar NaN en diferentes cálculos está bien documentada en el estándar IEEE 754, por lo que es probable que pueda hacer lo que hace el idioma en que está escrito su intérprete.

Por cierto: crear un lenguaje de programación que puedan utilizar los no programadores es algo que hemos intentado hacer desde 1964 (Dartmouth BASIC). Hasta ahora, no hemos tenido éxito. Pero buena suerte de todos modos.

Philipp
fuente
14
+1 gracias. Me convenciste de arrojar un error, y ahora que leí tu respuesta no entiendo por qué estaba dudando. PHPHa sido una mala influencia para mí.
Reactgular
24
Sí lo tiene. Cuando leí su pregunta, inmediatamente pensé que era algo muy similar a PHP producir resultados incorrectos y seguir avanzando en el camino ante los errores. Hay buenas razones por las que PHP es la excepción al hacer esto.
Joel
44
+1 para el comentario BÁSICO. No aconsejo usar NaNen un idioma para principiantes, pero en general, es una gran respuesta.
Ross Patterson
8
@Joel Si hubiera vivido lo suficiente, Dijkstra probablemente habría dicho "El uso de [PHP] paraliza la mente; por lo tanto, su enseñanza debería considerarse como un delito penal".
Ross Patterson
12
@Ross. "la arrogancia en informática se mide en nano-Dijkstras" - Alan Kay
33

1 - Ignorar el error y producir 0como resultado. Registrando una advertencia si es posible.

Esa no es una buena idea. En absoluto. La gente comenzará a depender de ello y si alguna vez lo arreglas, romperás mucho código.

2 - Agregue NaNcomo posible valor para los números, pero eso genera preguntas sobre cómo manejar los NaNvalores en otras áreas del lenguaje.

Debe manejar NaN de la misma manera que lo hacen los tiempos de ejecución de otros idiomas: cualquier cálculo adicional también produce NaN y cada comparación (incluso NaN == NaN) produce falso.

Creo que esto es aceptable, pero no necesariamente nuevo, amigable.

3 - Termine la ejecución del programa e informe al usuario que ocurrió un error grave.

Esta es la mejor solución, creo. Con esa información en mano, los usuarios deberían poder manejar 0. Debe proporcionar un entorno de prueba, especialmente si está destinado a ejecutarse una vez por noche.

También hay una cuarta opción. Haga de la división una operación ternaria. Cualquiera de estos dos funcionará:

  • div (numerador, denumerador, resultado alternativo)
  • div (numerador, denumerador, alternativo_denumerador)
back2dos
fuente
Pero si haces NaN == NaNser false, entonces tendrá que añadir una isNaN()función para que los usuarios son capaces de detectar NaNs.
AJMansfield
2
@AJMansfield: O eso, o personas que lo apliquen a sí mismos: isNan(x) => x != x. Aún así, cuando NaNaparezca su código de programación, no debe comenzar a agregar isNaNcontroles, sino más bien rastrear la causa y hacer los controles necesarios allí. Por lo tanto, es importante NaNpropagarse por completo.
back2dos
55
NaNs son principalmente contra-intuitivos. En el idioma de un principiante, están muertos a la llegada.
Ross Patterson
2
@RossPatterson Pero un principiante puede decir fácilmente 1/0: tienes que hacer algo con eso. No hay otro resultado posiblemente útil que no Infsea NaNalgo, algo que propague el error aún más en el programa. De lo contrario, la única solución es detenerse con un error en este punto.
Mark Hurd el
1
La opción 4 podría mejorarse permitiendo la invocación de una función, que a su vez podría realizar cualquier acción necesaria para recuperarse del divisor 0 inesperado.
CyberFonic
21

Termine la aplicación en ejecución con prejuicios extremos. (Mientras proporciona información de depuración adecuada)

Luego, eduque a sus usuarios para que identifiquen y manejen las condiciones donde el divisor podría ser cero (valores ingresados ​​por el usuario, etc.)

Dave Nay
fuente
13

En Haskell (y similar en Scala), en lugar de lanzar excepciones (o devolver referencias nulas) los tipos de contenedor Maybey Eitherpuede ser utilizado. Con Maybeel usuario tiene la oportunidad de probar si el valor que obtuvo está "vacío", o podría proporcionar un valor predeterminado al "desenvolver". Eitheres similar, pero se puede usar devuelve un objeto (por ejemplo, una cadena de error) que describe el problema si hay uno.

Landei
fuente
1
Es cierto, pero tenga en cuenta que Haskell no usa esto para la división por cero. En cambio, cada tipo de Haskell tiene implícitamente "fondo" como un valor posible. Esto no es como punteros nulos en el sentido de que es el "valor" de una expresión que no termina. No puede probar la no determinación como un valor, por supuesto, pero en la semántica operativa los casos que no terminan son parte del significado de una expresión. En Haskell, ese valor "inferior" también maneja resultados adicionales de casos de error, como la error "some message"función que se está evaluando.
Steve314
Personalmente, si el efecto de abortar todo el programa se considera válido, no sé por qué el código puro no puede tener el efecto de lanzar una excepción, pero ese soy yo, Haskellno permite que las expresiones puras arrojen excepciones.
Steve314
Creo que es una buena idea porque, aparte de lanzar una excepción, todas las opciones propuestas no comunican a los usuarios que cometieron un error. La idea básica es que el usuario comete un error con el valor que le dio al programa, por lo que el programa debe decirle al usuario que dio una entrada incorrecta (entonces el usuario puede pensar en una forma de remediarlo). Sin decirle a los usuarios sobre su error, cualquier solución se siente tan extraña.
InformadoA
Creo que este es el camino a seguir ... El lenguaje de programación Rust lo usa ampliamente en su biblioteca estándar.
aochagavia
12

Otras respuestas ya han considerado los méritos relativos de sus ideas. Propongo otro: use el análisis de flujo básico para determinar si una variable puede ser cero. Entonces puede simplemente no permitir la división por variables que son potencialmente cero.

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.

Alternativamente, tenga una función de aserción inteligente que establezca invariantes:

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

Esto es tan bueno como arrojar un error de tiempo de ejecución (evita completamente las operaciones indefinidas), pero tiene la ventaja de que la ruta del código ni siquiera necesita ser golpeada para que la falla potencial quede expuesta. Se puede hacer de manera muy similar a la verificación de tipos ordinaria, evaluando todas las ramas de un programa con entornos de escritura anidados para rastrear y verificar invariantes:

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

Además, se extiende naturalmente al rango y la nullcomprobación, si su idioma tiene tales características.

Jon Purdy
fuente
44
Buena idea, pero este tipo de resolución de restricciones es NP-completo. Imagina algo así def foo(a,b): return a / ord(sha1(b)[0]). El analizador estático no puede invertir SHA-1. Clang tiene este tipo de análisis estático y es ideal para encontrar errores superficiales, pero hay muchos casos que no puede manejar.
Mark E. Haase
99
esto no es NP completo, esto es imposible, dice un lema detenido. Sin embargo, el analizador estático no necesita resolver esto, solo puede resolver una declaración como esta y requerir que agregue una afirmación o decoración explícita.
MK01
1
@ MK01: En otras palabras, el análisis es "conservador".
Jon Purdy
11

El número 1 (insertar cero no borrable) siempre es malo. La elección entre # 2 (propagar NaN) y # 3 (eliminar el proceso) depende del contexto e idealmente debería ser una configuración global, como lo es en Numpy.

Si está haciendo un cálculo grande e integrado, propagar NaN es una mala idea porque eventualmente se extenderá e infectará todo su cálculo --- cuando mira los resultados en la mañana y ve que todos son NaN, usted ' tendría que descartar los resultados y comenzar de nuevo de todos modos. Hubiera sido mejor que el programa terminara, recibiera una llamada en medio de la noche y la arreglara --- en términos de la cantidad de horas desperdiciadas, al menos.

Si está haciendo muchos cálculos pequeños, en su mayoría independientes (como cálculos de reducción de mapas o vergonzosamente paralelos), y puede tolerar que algunos porcentajes de ellos sean inutilizables debido a NaN, esa es probablemente la mejor opción. Terminar el programa y no hacer el 99% que sería bueno y útil debido al 1% que tienen malformaciones y se dividen entre cero podría ser un error.

Otra opción, relacionada con los NaN: la misma especificación de punto flotante IEEE define Inf e -Inf, y estos se propagan de manera diferente que NaN. Por ejemplo, estoy bastante seguro de que Inf> cualquier número y -Inf <cualquier número, que sería lo que querrías si tu división por cero sucediera porque se suponía que el cero era un número pequeño. Si sus entradas son redondeadas y sufren un error de medición (como mediciones físicas tomadas a mano), la diferencia de dos grandes cantidades puede resultar en cero. Sin la división por cero, habría obtenido un gran número, y tal vez no le importa lo grande que sea. En ese caso, In y -Inf son resultados perfectamente válidos.

También puede ser formalmente correcto, solo di que estás trabajando en los reales extendidos.

Jim Pivarski
fuente
Pero no podemos decir si el denominador estaba destinado a ser positivo o negativo, por lo que la división podría producir + inf cuando se deseaba -inf, o viceversa.
Daniel Lubarov
Es cierto que su error de medición es demasiado pequeño para distinguir entre + inf y -inf. Esto se parece más a la esfera de Riemann, en la que todo el plano complejo se mapea a una bola con exactamente un punto infinito (el punto diametralmente opuesto al origen). Números positivos muy grandes, números negativos muy grandes e incluso números imaginarios y complejos muy grandes están todos cerca de ese punto infinito. Con un pequeño error de medición, no puede distinguirlos.
Jim Pivarski
Si está trabajando en ese tipo de sistema, tendría que identificar + inf y -inf como equivalentes, al igual que debe identificar +0 y -0 como equivalentes, aunque tengan representaciones binarias diferentes.
Jim Pivarski
8

3. Finalice la ejecución del programa e informe al usuario que se produjo un error grave.

[Esta opción] no es práctica ...

Por supuesto, es práctico: es responsabilidad de los programadores escribir un programa que realmente tenga sentido. Dividir por 0 no tiene ningún sentido. Por lo tanto, si el programador está realizando una división, también es su responsabilidad verificar de antemano que el divisor no sea igual a 0. Si el programador no realiza esa verificación de validación, entonces debe darse cuenta de ese error tan pronto como sea posible. posible, y los resultados de cálculo desnormalizados (NaN) o incorrectos (0) simplemente no ayudarán a ese respecto.

La opción 3 es la que te habría recomendado, por cierto, por ser la más directa, honesta y matemáticamente correcta.

stakx
fuente
4

Me parece una mala idea ejecutar tareas importantes (es decir, "cron nocturno") en un entorno donde se ignoran los errores. Es una idea terrible hacer de esto una característica. Esto descarta las opciones 1 y 2.

La opción 3 es la única solución aceptable. Las excepciones no tienen que ser parte del lenguaje, pero son parte de la realidad. Su mensaje de finalización debe ser lo más específico e informativo posible sobre el error.

ddyer
fuente
3

IEEE 754 en realidad tiene una solución bien definida para su problema. Manejo de excepciones sin usar exceptions http://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling

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

de esta manera todas sus operaciones tienen sentido matemáticamente.

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

En mi opinión, seguir el IEEE 754 tiene más sentido ya que garantiza que sus cálculos sean tan correctos como en una computadora y que también sea coherente con el comportamiento de otros lenguajes de programación.

El único problema que surge es que Inf y NaN van a contaminar sus resultados y sus usuarios no sabrán exactamente de dónde viene el problema. Eche un vistazo a un lenguaje como Julia que hace esto bastante 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

El error de división se propaga correctamente a través de las operaciones matemáticas, pero al final el usuario no necesariamente sabe de qué operación se deriva el error.

edit:No vi la segunda parte de la respuesta de Jim Pivarski, que es básicamente lo que digo anteriormente. Culpa mía.

Wallnuss
fuente
2

SQL, fácilmente el lenguaje más utilizado por los no programadores, ocupa el puesto # 3, por lo que sea que valga la pena. En mi experiencia observando y ayudando a los no programadores a escribir SQL, este comportamiento generalmente se entiende bien y se compensa fácilmente (con una declaración de caso o similar). Ayuda que el mensaje de error que recibes tiende a ser bastante directo, por ejemplo, en Postgres 9 obtienes "ERROR: división por cero".

Noah Yetter
fuente
2

Creo que el problema está "dirigido a usuarios novatos. -> Así que no hay soporte para ..."

¿Por qué cree que el manejo de excepciones es problemático para los usuarios novatos?

¿Qué es peor? ¿Tiene una característica "difícil" o no tiene idea de por qué sucedió algo? ¿Qué podría confundir más? ¿Un bloqueo con un volcado de núcleo o "Error fatal: dividir por cero"?

En cambio, creo que es MUCHO mejor apuntar a GRANDES errores de mensaje. En cambio, haga lo siguiente: "Cálculo incorrecto, Divida 0/0" (es decir: siempre muestre los DATOS que causan el problema, no solo el tipo de problema). Mira cómo PostgreSql hace los errores de mensaje, eso es genial en mi humilde opinión.

Sin embargo, puede buscar otras formas de trabajar con excepciones como:

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

También he soñado con construir un lenguaje, y en este caso creo que mezclar un tal vez / opcional con excepciones normales podría ser lo mejor:

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')
mamcx
fuente
1

En mi opinión, su idioma debería proporcionar un mecanismo genérico para detectar y manejar errores. Los errores de programación deben detectarse en el momento de la compilación (o tan pronto como sea posible) y normalmente deben conducir a la finalización del programa. Los errores que resultan de datos inesperados o erróneos, o de condiciones externas inesperadas, deben detectarse y ponerse a disposición para la acción adecuada, pero permiten que el programa continúe siempre que sea posible.

Las acciones plausibles incluyen (a) terminar (b) solicitar al usuario una acción (c) registrar el error (d) sustituir un valor corregido (e) establecer un indicador para probar en el código (f) invocar una rutina de manejo de errores. ¿Cuáles de estos pone a disposición y por qué medios son las elecciones que tiene que hacer?

Según mi experiencia, los errores de datos comunes, como las conversiones defectuosas, la división por cero, el desbordamiento y el valor fuera de rango, son benignos y, por defecto, deben manejarse sustituyendo un valor diferente y configurando un indicador de error. El (no programador) que use este lenguaje verá los datos defectuosos y comprenderá rápidamente la necesidad de verificar los errores y manejarlos.

[Por ejemplo, considere una hoja de cálculo de Excel. Excel no termina su hoja de cálculo porque un número se desbordó o lo que sea. La celda tiene un valor extraño y vas a averiguar por qué y arreglarlo.]

Entonces, para responder a su pregunta: ciertamente no debe terminar. Puede sustituir NaN pero no debe hacerlo visible, solo asegúrese de que el cálculo se complete y genere un valor alto extraño. Y configure un indicador de error para que los usuarios que lo necesiten puedan determinar que se produjo un error.

Divulgación: Creé tal implementación de lenguaje (Powerflex) y abordé exactamente este problema (y muchos otros) en la década de 1980. Ha habido poco o ningún progreso en los idiomas para los no programadores en los últimos 20 años más o menos, y atraerá un montón de críticas por intentarlo, pero realmente espero que tenga éxito.

david.pfx
fuente
1

Me gustó el operador ternario donde proporciona un valor alternativo en caso de que el denumerador sea 0.

Una idea más que no vi es producir un valor "inválido" general. Un general "esta variable no tiene un valor porque el programa hizo algo malo", que lleva consigo un seguimiento completo de la pila. Luego, si alguna vez usa ese valor en alguna parte, el resultado es nuevamente inválido, con la nueva operación intentada en la parte superior (es decir, si el valor inválido aparece alguna vez en una expresión, la expresión completa arroja un valor inválido y no se intentan llamadas a funciones; una excepción sería ser operadores booleanos: verdadero o inválido es verdadero y falso e inválido es falso, también puede haber otras excepciones). Una vez que ya no se hace referencia a ese valor en ninguna parte, registra una descripción larga y agradable de toda la cadena donde las cosas estaban mal y continúa con los negocios como de costumbre. Tal vez envíe el rastro al líder del proyecto o algo así.

Algo como la mónada tal vez básicamente. Funcionará con cualquier otra cosa que también pueda fallar, y puede permitir que las personas construyan sus propios inválidos. Y el programa continuará ejecutándose mientras el error no sea demasiado profundo, que es lo que realmente se quiere aquí, creo.

Moshev
fuente
1

Hay dos razones fundamentales para dividir por cero.

  1. En un modelo preciso (como enteros), obtiene una división entre cero DBZ porque la entrada es incorrecta. Este es el tipo de DBZ en el que la mayoría de nosotros pensamos.
  2. En un modelo no preciso (como el punto flotante), puede obtener un DBZ debido al error de redondeo aunque la entrada sea válida. Esto es lo que normalmente no pensamos.

Para 1. usted debe comunicar a los usuarios que cometieron un error porque ellos son los responsables y ellos son quienes mejor saben cómo remediar la situación.

Para 2. Esto no es culpa del usuario, puede señalar con el dedo el algoritmo, la implementación de hardware, etc., pero esto no es culpa del usuario, por lo que no debe terminar el programa ni siquiera lanzar una excepción (si está permitido, lo que no es así en este caso). Entonces, una solución razonable es continuar las operaciones de alguna manera razonable.

Puedo ver a la persona que hace esta pregunta para el caso 1. Por lo tanto, debe comunicarse con el usuario. Usando cualquier estándar de punto flotante, Inf, -Inf, Nan, IEEE no encaja en esta situación. Estrategia fundamentalmente incorrecta.

InformadoA
fuente
0

No lo permita en el idioma. Es decir, no permita dividir por un número hasta que sea probable que no sea cero, generalmente probándolo primero. Es decir.

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero
MSalters
fuente
Para hacer esto, necesita un nuevo tipo numérico, un número natural, en lugar de un número entero. Eso podría ser ... difícil ... de tratar.
Servicio
@Servy: No, no lo harías. ¿Por que lo harias? Necesita lógica en el compilador para determinar los posibles valores, pero lo desea de todos modos (por razones de optimización).
MSalters
Si no tiene un tipo diferente, uno para cero y otro para valores distintos de cero, entonces no podrá resolver el problema en el caso general. Tendría falsos positivos y obligaría al usuario a verificar cero de manera más frecuente de lo que realmente debería, o creará situaciones en las que aún pueden dividirse por cero.
Servicio
@Servy: Estás equivocado: un compilador puede rastrear ese estado sin necesidad de ese tipo, y por ejemplo GCC ya lo hace. Por ejemplo, el tipo C intpermite valores cero, pero GCC aún puede determinar en qué parte del código las entradas específicas no pueden ser cero.
MSalters
2
Pero solo en ciertos casos; no puede hacerlo, con 100% de precisión, en todos los casos. Tendrás falsos positivos o falsos negativos. Esto es demostrablemente cierto. Por ejemplo, podría crear un fragmento de código que puede o no completarse . Si el compilador ni siquiera puede saber si terminó, ¿cómo podría saber si el int resultante no es cero? Puede detectar casos simples y obvios, pero no todos los casos.
Servicio
0

Al escribir un lenguaje de programación, debe aprovechar el hecho y hacer obligatorio incluir una acción para el dispositivo por estado cero. a <= n / c: 0 div-by-zero-action

Sé que lo que acabo de sugerir es esencialmente agregar un 'goto' a su PL.

Stephen
fuente