¿Cómo se deben informar los errores en las bibliotecas científicas?

11

Existen muchas filosofías en diferentes disciplinas de ingeniería de software sobre cómo las bibliotecas deben hacer frente a errores u otras condiciones excepcionales. Algunos de los que he visto:

  1. Devuelve un código de error con el resultado devuelto por un argumento puntero. Esto es lo que hace PETSc.
  2. Devuelve errores por un valor centinela. Por ejemplo, malloc devuelve NULL si no puede asignar memoria, sqrtdevolverá NaN si pasa un número negativo, etc. Este enfoque se utiliza en muchas funciones de libc.
  3. Lanzar excepciones. Utilizado en deal.II, Trilinos, etc.
  4. Devuelve un tipo de variante; por ejemplo, una función de C ++ que devuelve un objeto de tipo Resultsi se ejecuta correctamente y usa un tipo Errorpara describir cómo volvería a fallar std::variant<Error, Result>.
  5. Utilice afirmar y bloquearse. Usado en p4est y algunas partes de igraph.

Problemas con cada enfoque:

  1. La comprobación de cada error introduce muchos códigos adicionales. Los valores en los que se almacenará un resultado siempre deben declararse primero, introduciendo muchas variables temporales que solo pueden usarse una vez. Este enfoque explica qué error ocurrió, pero puede ser difícil determinar por qué o, para una pila de llamadas profundas, dónde.
  2. El caso de error es fácil de ignorar. Además de eso, muchas funciones ni siquiera pueden tener un valor centinela significativo si todo el rango de tipos de salida es un resultado plausible. Muchos de los mismos problemas que # 1.
  3. Solo es posible en C ++, Python, etc., no en C o Fortran. Se puede imitar en C usando la brujería setjmp / longjmp o libunwind .
  4. Solo es posible en C ++, Rust, OCaml, etc., no en C o Fortran. Se puede imitar en C usando macro brujería.
  5. Posiblemente el más informativo. Pero si adopta este enfoque para, por ejemplo, una biblioteca C para la que luego escribe un contenedor de Python, un error tonto como pasar un índice fuera de los límites a una matriz bloqueará el intérprete de Python.

Gran parte de los consejos en Internet sobre el manejo de errores se escriben desde el punto de vista de los sistemas operativos, el desarrollo integrado o las aplicaciones web. Los bloqueos son inaceptables y debe preocuparse por la seguridad. Las aplicaciones científicas no tienen estos problemas en la misma medida, si es que lo hacen.

Otra consideración es qué tipos de errores son recuperables o no. Un error de malloc no es recuperable y, en cualquier caso, el asesino de falta de memoria del sistema operativo lo resolverá antes que usted. Un índice fuera de los límites para un tamaño de matriz tampoco es recuperable. Para mí, como usuario, lo mejor que puede hacer una biblioteca es bloquearse con un mensaje de error informativo. Por otro lado, la falla de, por ejemplo, un solucionador lineal iterativo para converger podría recuperarse usando un solucionador de factorización directa.

¿Cómo deberían las bibliotecas científicas informar errores y esperar que se manejen? Por supuesto, me doy cuenta de que depende del idioma en que se implemente la biblioteca. Pero, por lo que puedo decir, para cualquier biblioteca lo suficientemente útil, la gente querrá llamarlo desde otro idioma que no sea el que está implementado.

Como comentario aparte, creo que el enfoque n. ° 5 se puede mejorar sustancialmente para una biblioteca C si define un puntero de función de controlador de aserción global como parte de la API pública. El controlador de aserción sería predeterminado para informar el número de archivo / línea y el bloqueo. Los enlaces de C ++ para esta biblioteca definirían un nuevo controlador de aserciones que en su lugar arroja una excepción de C ++. Del mismo modo, los enlaces de Python definirían un controlador de aserciones que usa la API de CPython para lanzar una excepción de Python. Pero no conozco ningún ejemplo que tome este enfoque.

Daniel Shapero
fuente
Otra consideración son las ramificaciones de rendimiento. ¿Cómo afectan estos diversos métodos a la velocidad del software? ¿Deberíamos usar un manejo de errores diferente en las partes de "control" del código (por ejemplo, procesar archivos de entrada) versus los "motores" computacionalmente caros?
LedHead
Tenga en cuenta que la mejor respuesta diferirá según el idioma.
chrylis -on strike-

Respuestas:

10

Le daré mi perspectiva, que está codificada en el acuerdo. II proyecto al que hace referencia.

Primero, hay dos tipos de condiciones de error: errores que se pueden recuperar y errores que no se pueden recuperar.

  • El primero es, por ejemplo, si un archivo de entrada no se puede leer, por ejemplo, si está leyendo información de un archivo como $HOME/.dealiiese puede o no existir. La función de lectura debería volver a la función de llamada para que esta última descubra qué hacer. También puede ser que un recurso no esté disponible en este momento, pero puede estar nuevamente en un minuto (un sistema de archivos montado de forma remota).

  • La última es, por ejemplo, si está tratando de agregar un vector de tamaño 10 a un vector de tamaño 20: intente lo que pueda, no hay nada que se pueda hacer al respecto: hay un error en el código que condujo a El punto donde intentamos hacer la adición.

Estas dos condiciones deben tratarse de manera diferente, independientemente del lenguaje de programación que esté utilizando:

  • En el segundo caso, dado que no hay recurso, finalice el programa. Puede hacerlo lanzando una excepción o devolviendo un código de error que le indica a la persona que llama que no se puede hacer nada, pero también podría abortar el programa de inmediato, ya que eso hace que sea mucho más fácil para el programador depurar el problema.

  • En el primer caso, ha surgido una situación excepcional que podría manejarse. A pesar de que C y Fortran no tenían medios para expresar esto, todos los lenguajes razonables que vinieron más tarde han incorporado formas en el estándar del lenguaje para tratar tales retornos "excepcionales" al proporcionar, bueno, "excepciones". Úselos: para eso están allí; También están diseñados de tal manera que no puede olvidarse de ignorarlos (si lo hace, la excepción solo propaga un nivel más alto).

En otras palabras, lo que estoy defendiendo aquí (y lo que hace deal.II) es una mezcla de sus estrategias 3 y 5, según el contexto. Es cierto que 3 no funciona en lenguajes como C o Fortran, en cuyo caso se puede argumentar que esa es una buena razón para no usar lenguajes que dificultan la expresión de lo que quieres hacer.

x), pero dado que el evaluador necesita ser llamado repetidamente, no solo debe fallar sino simplemente lanzar una excepción. En tales casos, aunque pasar un valor negativo no es recuperable, uno debería lanzar una excepción en lugar de abortar el programa. Había estado en desacuerdo con esta postura hace un par de años, pero he cambiado de opinión después de que las pautas del software de la comunidad xSDK codificaran el requisito de que los programas nunca deberían fallar (o al menos deberían tener una forma de pasar del bloqueo a la excepción), así que trata. II ahora tiene la opción de hacer Assertlanzar una excepción en lugar de llamar abort()).

Wolfgang Bangerth
fuente
Solo recomendaría lo contrario: lanzar una excepción cuando la situación no se pueda manejar y devolver un código de error cuando se pueda manejar. El problema es que lidiar con las excepciones lanzadas es complicado: el programador de la aplicación debe conocer el tipo de todas las excepciones posibles para detectarlas y manejarlas; de lo contrario, el programa simplemente se bloqueará. El bloqueo está bien e incluso es bienvenido para situaciones que no se pueden manejar, porque el punto de bloqueo se informa de fábrica con python, por ejemplo, pero para situaciones que se pueden manejar, (en su mayoría) no es bienvenido.
cdalitz
@cdalitz: es un defecto de diseño de C ++ que puedes lanzar objetos de cualquier tipo. Pero cualquier software razonable (excluido Trilinos) solo arroja excepciones que se derivan std::exception, y estos pueden ser capturados por referencia sin conocer el tipo derivado.
Wolfgang Bangerth
1
Pero estoy totalmente en desacuerdo con devolver un código de error por los motivos descritos en la pregunta original: (i) Los códigos de error se ignoran con demasiada frecuencia y, en consecuencia, los errores no se manejan en absoluto; (ii) en muchos casos, simplemente no hay un valor excepcional que pueda devolverse razonablemente dado que el tipo de retorno de la función es fijo; (iii) las funciones tienen diferentes tipos de retorno, y tendría que definir en cada caso por separado cuál sería el valor "excepcional" que representa un error.
Wolfgang Bangerth
WB escribió (lo siento, el truco '@' no funciona por alguna razón y StackExchage elimina el nombre de usuario por alguna razón): "Los códigos de error se ignoran con demasiada frecuencia". Esto vale aún más para la captura de excepciones: no muchos desarrolladores de software se toman la molestia de poner entre corchetes cada llamada de función en un bloque try / catch. Pero es sobre todo una cuestión de gustos: siempre que la documentación indique claramente si y qué excepciones arroja una función, puedo manejarlo. Pero nuevamente podría decirse: el deber de escribir documentación se ignora con demasiada frecuencia ;-)
cdalitz
Pero el punto es que si olvida detectar una excepción, entonces no hay problemas posteriores: el programa simplemente aborta. Será fácil encontrar dónde ocurrió el problema. Si olvida verificar el código de error, su programa puede fallar en algún momento posterior debido a un estado interno indefinido, pero el problema original sigue sin estar completamente claro. Es extremadamente difícil encontrar este tipo de errores.
Wolfgang Bangerth