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.
- Ignorar el error y producir
0
como resultado. Registrando una advertencia si es posible. - Agregue
NaN
como un posible valor para los números, pero eso genera preguntas sobre cómo manejar losNaN
valores en otras áreas del lenguaje. - 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?
fuente
reject "Foo"
se implementó, sino simplemente que rechaza un documento si contiene la palabra claveFoo
. 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.Respuestas:
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.
fuente
PHP
Ha sido una mala influencia para mí.NaN
en un idioma para principiantes, pero en general, es una gran respuesta.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.
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.
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á:
fuente
NaN == NaN
serfalse
, entonces tendrá que añadir unaisNaN()
función para que los usuarios son capaces de detectarNaN
s.isNan(x) => x != x
. Aún así, cuandoNaN
aparezca su código de programación, no debe comenzar a agregarisNaN
controles, sino más bien rastrear la causa y hacer los controles necesarios allí. Por lo tanto, es importanteNaN
propagarse por completo.NaN
s son principalmente contra-intuitivos. En el idioma de un principiante, están muertos a la llegada.1/0
: tienes que hacer algo con eso. No hay otro resultado posiblemente útil que noInf
seaNaN
algo, 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.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.)
fuente
En Haskell (y similar en Scala), en lugar de lanzar excepciones (o devolver referencias nulas) los tipos de contenedor
Maybe
yEither
puede ser utilizado. ConMaybe
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 devuelve un objeto (por ejemplo, una cadena de error) que describe el problema si hay uno.fuente
error "some message"
función que se está evaluando.Haskell
no permite que las expresiones puras arrojen excepciones.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.
Alternativamente, tenga una función de aserción inteligente que establezca invariantes:
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:
Además, se extiende naturalmente al rango y la
null
comprobación, si su idioma tiene tales características.fuente
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.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.
fuente
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.
fuente
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.
fuente
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_handlingde 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.
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.fuente
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".
fuente
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:
fuente
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.
fuente
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.
fuente
Hay dos razones fundamentales para dividir por cero.
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.
fuente
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.
fuente
int
permite valores cero, pero GCC aún puede determinar en qué parte del código las entradas específicas no pueden ser cero.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.
fuente