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

61

Estoy desarrollando un nuevo lenguaje de programación para resolver algunos requisitos comerciales, y este lenguaje está dirigido a usuarios novatos. Así que no hay soporte para el manejo de excepciones en el idioma, y no esperaría que lo usaran incluso si lo añadiera.

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. Ignore el error y genere 0 como resultado. Registrar una advertencia si es posible.
  2. Agregue NaN como un posible valor para los números, pero eso genera preguntas sobre cómo manejar los valores de NaN en otras áreas del idioma.
  3. Termine la ejecución del programa e informe al usuario que ocurrió un error grave.

La opció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 con la opción # 1?

    
pregunta cgTag 18.08.2013 - 13:45

16 respuestas

98

Recomiendo encarecidamente no # 1, porque ignorar los errores es un anti-patrón peligroso. Puede llevar a errores difíciles de analizar. Establecer el resultado de una división de 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 está ejecutando 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), abortarlo y mantener todo tal como está es generalmente preferido a llenar su base de datos con basura.

Además, es poco probable que tenga éxito si sigue a fondo 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 (usar 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 probablemente pueda hacer lo mismo que hace el idioma en que está escrito su intérprete.

Por cierto: La creación de un lenguaje de programación utilizable por no programadores es algo que hemos estado tratando de hacer desde 1964 (Dartmouth BASIC). Hasta ahora, no hemos tenido éxito. Pero buena suerte de todos modos.

    
respondido por el Philipp 18.08.2013 - 14:48
33
  

1: ignora el error y produce 0 como resultado. Registrar una advertencia si es posible.

No es una buena idea. En absoluto. La gente empezará a depender de eso y si alguna vez lo arreglas, romperás mucho código.

  

2: agregue NaN como un posible valor para los números, pero eso genera preguntas sobre cómo manejar los valores de NaN en otras áreas del idioma.

Debe manejar NaN de la forma en que lo hacen los tiempos de ejecución de otros idiomas: cualquier cálculo adicional también arroja NaN y todas las comparaciones (incluso NaN == NaN) son falsas.

Creo que esto es aceptable, pero no necesariamente amigable para los recién llegados.

  

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 la mano, los usuarios deben poder manejar 0. Debes proporcionar un entorno de prueba, especialmente si está pensado para ejecutarse una vez por noche.

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

  • div (numerador, denumerador, alternative_result)
  • div (numerator, denumerator, alternative_denumerator)
respondido por el back2dos 18.08.2013 - 15:43
20

Termine la aplicación en ejecución con extremo prejuicio. (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.)

    
respondido por el Dave Nay 18.08.2013 - 15:23
12

En Haskell (y similar en Scala), en lugar de lanzar excepciones (o devolver referencias nulas), se pueden usar los tipos de envoltorio Maybe y Either . Con Maybe , el usuario tiene la oportunidad de probar si el valor que obtuvo está "vacío", o podría proporcionar un valor predeterminado al "desenvolver". Either es similar, pero se puede usar para devolver un objeto (por ejemplo, una cadena de error) que describe el problema, si existe.

    
respondido por el Landei 18.08.2013 - 18:34
12

Otras respuestas ya han considerado los méritos relativos de sus ideas. Propongo otra: usar el análisis de flujo básico para determinar si una variable puede ser cero. Luego, simplemente puede rechazar 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 afirmació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 lanzar un error de tiempo de ejecución, usted omite por completo las operaciones no definidas, pero tiene la ventaja de que la ruta del código no necesita ser golpeada para que la falla potencial esté expuesta. Se puede hacer de forma muy similar a la verificación de tipos ordinaria, al evaluar todas las ramas de un programa con entornos de escritura anidados para el seguimiento y verificación de 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 a la verificación de rango y null , si su idioma tiene tales características.

    
respondido por el Jon Purdy 18.08.2013 - 23:23
10

El número 1 (inserte el cero no descargable) 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, la propagación de NaN es una mala idea, ya que eventualmente se propagará e infectará todo su cálculo, cuando mire los resultados en la mañana y vea que todos son NaN. , tendrías que tirar los resultados y comenzar de nuevo de todos modos. Hubiera sido mejor si el programa terminara, recibiste una llamada en medio de la noche y la arreglaste, por lo menos en términos de horas desperdiciadas.

Si está haciendo muchos cálculos pequeños, en su mayoría independientes (como cálculos de reducción de mapa o paralelos vergonzosamente), y puede tolerar que un porcentaje de ellos sea inutilizable debido a los NaN, probablemente esa sea la mejor opción. Terminar el programa y no hacer el 99% que sería bueno y útil debido al 1% que está mal formado y dividir 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 a NaN. Por ejemplo, estoy bastante seguro de que Inf > cualquier número y -Inf < cualquier número, que sería lo que desea si su división por cero sucediera porque se suponía que el cero era solo un número pequeño. Si sus entradas están redondeadas y sufren un error de medición (como medidas físicas tomadas a mano), la diferencia de dos grandes cantidades puede resultar en cero. Sin la división por cero, habrías obtenido un gran número y tal vez no te importe 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.

    
respondido por el Jim Pivarski 18.08.2013 - 22:33
8
  

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

     

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

Por supuesto que es práctico: es responsabilidad de los programadores escribir un programa que realmente tenga sentido. La división 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 es igual a 0. Si el programador no realiza esa verificación de validación, entonces él / ella debería darse cuenta de ese error tan pronto como sea posible, y los resultados de cálculo desnormalizados (NaN) o incorrectos (0) simplemente no ayudarán en ese sentido.

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

    
respondido por el stakx 19.08.2013 - 11:11
4

Me parece una mala idea ejecutar tareas importantes (es decir, "cron nocturno") en un entorno donde los errores son ignorados. Es una idea terrible hacer 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, sino que son parte de la realidad. Su mensaje de finalización debe ser lo más específico e informativo posible sobre el error.

    
respondido por el ddyer 21.08.2013 - 21:08
3

IEEE 754 en realidad tiene una solución bien definida para su problema. Manejo de excepciones sin usar exceptions enlace

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

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

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

En mi opinión, seguir IEEE 754 es lo más sensato, ya que garantiza que sus cálculos sean tan correctos como los de 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 proviene 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 proviene el error.

edit: No vi la segunda parte de la respuesta de Jim Pivarski, que es básicamente lo que estoy diciendo arriba. Mi mal.

    
respondido por el wallnuss 13.07.2014 - 09:29
2

Creo que el problema está "dirigido a usuarios novatos. - > Por lo tanto, no hay soporte para ..."

¿Por qué piensa 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 fallo con un volcado de núcleo o "Error fatal: dividir por cero"?

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

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

enlace

También he soñado con crear 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')
    
respondido por el mamcx 13.07.2014 - 06:45
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 terminación del programa. Los errores que resultan de datos inesperados o erróneos, o de condiciones externas inesperadas, deben detectarse y estar disponibles para la acción apropiada, pero permiten que el programa continúe siempre que sea posible.

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

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

[Para un 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 obtiene un valor extraño y vas a averiguar por qué y lo arreglas.]

Entonces, para responder a tu pregunta: ciertamente no debes terminar. Puede sustituir NaN pero no debe hacer que esté visible, solo asegúrese de que el cálculo se complete y genere un valor alto extraño. Y establezca un indicador de error para que los usuarios que lo necesiten puedan determinar que ocurrió un error.

Divulgación: yo creé una implementación de lenguaje (Powerflex) y resolví 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 aproximadamente, y atraerá muchas críticas por intentarlo, pero realmente espero que tenga éxito.

    
respondido por el david.pfx 12.07.2014 - 13:18
1

SQL, fácilmente el lenguaje más utilizado por los no programadores, hace el # 3, por lo que valga. En mi experiencia al observar y ayudar a los no programadores a escribir SQL, este comportamiento generalmente se comprende bien y se compensa fácilmente (con una declaración de un caso o algo similar). Ayuda que el mensaje de error que recibe tiende a ser bastante directo, por ejemplo. en Postgres 9 obtienes "ERROR: división por cero".

    
respondido por el Noah Yetter 13.07.2014 - 06:28
1

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

Una idea más que no vi es producir un valor general "no válido". Un general "esta variable no tiene un valor porque el programa hizo algo malo", que lleva consigo un seguimiento completo de la pila. Entonces, si alguna vez usa ese valor en cualquier lugar, el resultado vuelve a ser 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 no es válida y no se intentará realizar ninguna llamada a una función; ser operadores booleanos - verdadero o inválido es verdadero y falso e inválido es falso - puede haber otras excepciones también. Una vez que ya no se hace referencia a ese valor en ninguna parte, registre una descripción larga y agradable de toda la cadena en la que las cosas no funcionaron y continúe funcionando como siempre. Tal vez envíe por correo electrónico la traza al líder del proyecto o algo así.

Algo así 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 siempre que el error no sea demasiado profundo, que es lo que realmente se quiere aquí, creo.

    
respondido por el Moshev 13.07.2014 - 09:05
1

Hay dos razones fundamentales para una división por cero.

  1. En un modelo preciso (como los enteros), obtienes una división por cero DBZ porque la entrada es incorrecta. Este es el tipo de DBZ que la mayoría de nosotros pensamos.
  2. En un modelo no preciso (como un elemento flotante), es posible que obtenga un DBZ debido a un error de redondeo aunque la entrada sea válida. Esto es lo que normalmente no pensamos.

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

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

Puedo ver a la persona que hace esta pregunta en 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 errónea.

    
respondido por el InformedA 13.07.2014 - 10:11
0

No lo permitas en el idioma. Es decir, no permitir que se divida entre un número hasta que sea probablemente cero, por lo general, probándolo primero. Es decir.

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero
    
respondido por el MSalters 15.11.2013 - 17:52
0

Mientras escribe 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 tu PL.

    
respondido por el Stephen 13.07.2014 - 21:02

Lea otras preguntas en las etiquetas