Como os erros devem ser relatados nas bibliotecas científicas?

11

Existem muitas filosofias em diferentes disciplinas de engenharia de software sobre como as bibliotecas devem lidar com erros ou outras condições excepcionais. Alguns dos que eu já vi:

  1. Retorne um código de erro com o resultado retornado por um argumento de ponteiro. É isso que o PETSc faz.
  2. Retornar erros por um valor sentinela. Por exemplo, malloc retorna NULL se não puder alocar memória, sqrtretornará NaN se você passar um número negativo, etc. Essa abordagem é usada em muitas funções libc.
  3. Lance exceções. Usado no negócio. II, Trilinos, etc.
  4. Retornar um tipo de variante; por exemplo, uma função C ++ que retorna um objeto do tipo Resultse ele for executado corretamente e usa um tipo Errorpara descrever como ele falharia std::variant<Error, Result>.
  5. Use assert e travar. Usado no p4est e em algumas partes do igraph.

Problemas com cada abordagem:

  1. A verificação de todos os erros introduz muito código extra. Os valores nos quais um resultado será armazenado sempre precisam ser declarados primeiro, introduzindo muitas variáveis ​​temporárias que podem ser usadas apenas uma vez. Essa abordagem explica qual erro ocorreu, mas pode ser difícil determinar por que ou, para uma pilha profunda de chamadas, onde.
  2. O caso de erro é fácil de ignorar. Além disso, muitas funções não podem ter um valor sentinela significativo se todo o intervalo de tipos de saída for um resultado plausível. Muitos dos mesmos problemas que o nº 1.
  3. Só é possível em C ++, Python, etc., não em C ou Fortran. Pode ser imitado em C usando a feitiçaria setjmp / longjmp ou libunwind .
  4. Somente possível em C ++, Rust, OCaml etc., não em C ou Fortran. Pode ser imitado em C usando macro feitiçaria.
  5. Indiscutivelmente o mais informativo. Mas se você adotar essa abordagem para, digamos, uma biblioteca C para a qual você escreve um wrapper Python, um erro bobo como passar um índice fora dos limites para uma matriz causará um erro no interpretador Python.

Muitos dos conselhos na internet sobre tratamento de erros são escritos do ponto de vista de sistemas operacionais, desenvolvimento incorporado ou aplicativos da Web. Falhas são inaceitáveis ​​e você precisa se preocupar com segurança. As aplicações científicas não têm esses problemas quase na mesma extensão, se houver.

Outra consideração é que tipos de erros são recuperáveis ​​ou não. Uma falha de malloc não é recuperável e, em qualquer caso, o killer de falta de memória do sistema operacional chegará a ela antes de você. Um índice fora dos limites para um tamanho de matriz também não é recuperável. Para mim, como usuário, a melhor coisa que uma biblioteca pode fazer é travar com uma mensagem de erro informativa. Por outro lado, a falha de, digamos, um solucionador linear iterativo para convergir pode ser recuperada usando um solucionador de fatoração direta.

Como as bibliotecas científicas devem relatar erros e esperar que sejam tratados? Percebo, é claro, que depende do idioma em que a biblioteca está implementada. Mas, tanto quanto posso dizer, para qualquer biblioteca suficientemente útil, as pessoas vão querer chamá-lo de algum idioma diferente daquele em que está implementado.

Como um aparte, acho que a abordagem nº 5 pode ser aprimorada substancialmente para uma biblioteca C se ela definir um ponteiro de função do manipulador de asserção global como parte da API pública. O manipulador de asserções usará como padrão o relatório de número de arquivo / linha e falha. As ligações C ++ para esta biblioteca definiriam um novo manipulador de asserção que, em vez disso, lança uma exceção C ++. Da mesma forma, as ligações Python definiriam um manipulador de asserção que usa a API CPython para gerar uma exceção Python. Mas não conheço exemplos que adotem essa abordagem.

Daniel Shapero
fonte
Outra consideração são as ramificações de desempenho. Como esses vários métodos afetam a velocidade do software? Devemos usar manipulação de erro diferente em partes de "controle" do código (por exemplo, processamento de arquivos de entrada) versus os "mecanismos" computacionalmente caros?
LedHead 18/09/19
Observe que a melhor resposta será diferente por idioma.
usar o seguinte comando

Respostas:

10

Darei a você minha perspectiva, que está codificada no projeto deal.II que você faz referência.

Primeiro, existem dois tipos de condições de erro: Erros que podem ser recuperados e erros que não podem ser recuperados.

  • O primeiro é, por exemplo, se um arquivo de entrada não pode ser lido - por exemplo, se você está lendo informações de um arquivo como $HOME/.dealiiesse que pode ou não existir. A função de leitura deve retornar à função de chamada para que ela descubra o que fazer. Também pode ser que um recurso não esteja disponível no momento, mas esteja novamente em um minuto (um sistema de arquivos montado remotamente).

  • O último é, por exemplo, se você estiver tentando adicionar um vetor de tamanho 10 a um vetor de tamanho 20: Tente o quanto puder, não há nada que possa ser feito sobre isso - há um erro no código que levou a o ponto em que tentamos fazer a adição.

Essas duas condições devem ser tratadas de maneira diferente, independentemente da linguagem de programação que você está usando:

  • No segundo caso, como não há recurso, encerre o programa. Você pode fazer isso lançando uma exceção ou retornando um código de erro que indica ao chamador que nada pode ser feito, mas é possível abortar o programa imediatamente, pois isso facilita muito o programador para depurar o problema.

  • No primeiro caso, surgiu uma situação excepcional que poderia ser tratada. Embora C e Fortran não tivessem meios de expressar isso, todas as linguagens razoáveis ​​que vieram mais tarde incorporaram maneiras no padrão de linguagem para lidar com retornos "excepcionais", fornecendo, bem, "exceções". Use-os - é para isso que eles servem; eles também são projetados de tal maneira que você não pode esquecer de ignorá-los (se o fizer, a exceção propaga um nível mais alto).

Em outras palavras, o que estou defendendo aqui (e o que o acordo.II faz) é uma mistura de suas estratégias 3 e 5, dependendo do contexto. É verdade que o 3 não funciona em idiomas como C ou Fortran - nesse caso, pode-se argumentar que esse é um bom motivo para não usar idiomas que dificultam a expressão do que você deseja fazer.

x), mas como o avaliador precisa ser chamado repetidamente, não deve apenas travar, mas apenas lançar uma exceção. Nesses casos, mesmo que a transmissão de um valor negativo não seja recuperável, deve-se lançar uma exceção em vez de abortar o programa. Eu discordei dessa postura há alguns anos, mas mudei de idéia depois que as diretrizes do software da comunidade xSDK codificaram o requisito de que os programas nunca deveriam travar (ou pelo menos deveriam ter uma maneira de mudar de travamento para exceção). II agora tem a opção de fazer Assertuma exceção ao invés de chamar abort().)

Wolfgang Bangerth
fonte
Eu recomendaria apenas o oposto: lançar uma exceção quando a situação não puder ser tratada e retornar um código de erro quando puder ser tratado. O problema é que lidar com exceções lançadas é complicado: o programador de aplicativos deve saber o tipo de todas as exceções possíveis para capturá-las e manipulá-las; caso contrário, o programa irá travar. Falha é aceitável e até bem-vinda em situações que não podem ser tratadas, porque o ponto de falha é relatado imediatamente com python, por exemplo, mas para situações que podem ser tratadas, não é (na maioria das vezes) bem-vindo.
Cdalitz 18/09/19
@daldal: É uma falha de design do C ++ que você pode lançar objetos de qualquer tipo. Porém, qualquer software razoável (excluído o Trilinos) apenas lança exceções derivadas std::exceptione elas podem ser capturadas por referência sem conhecer o tipo derivado.
Wolfgang Bangerth 18/09/19
1
Mas eu discordo totalmente do retorno de um código de erro pelos motivos descritos na pergunta original: (i) Os códigos de erro são ignorados com muita frequência e, como conseqüência, os erros não são tratados; (ii) em muitos casos, simplesmente não existe um valor excepcional que possa ser razoavelmente retornado, dado que o tipo de retorno da função é fixo; (iii) funções têm diferentes tipos de retorno e você deve definir em cada caso separadamente qual seria o valor "excepcional" que representa um erro.
Wolfgang Bangerth 18/09/19
O WB escreveu (desculpe, o truque '@' não funciona por algum motivo e o nome de usuário é removido pelo StackExchage por algum motivo): "Os códigos de erro são ignorados com muita frequência". Isso vale ainda mais para a captura de exceções: poucos desenvolvedores de software se dão ao trabalho de agrupar todas as chamadas de função em um bloco try / catch. Mas é principalmente uma questão de gosto: desde que a documentação indique claramente se e quais exceções uma função gera, eu posso lidar com isso. Mas, novamente, poderia ser dito: o dever de documentação escrita é ignorado demasiadas vezes ;-)
cdalitz
Mas o ponto é que, se você esquecer uma exceção, não haverá problemas posteriores: o programa é abortado. Vai ser fácil encontrar onde o problema aconteceu. Se você esquecer de verificar o código de erro, seu programa poderá falhar em algum momento posterior devido a um estado interno indefinido - mas onde o problema original foi totalmente incerto. É extremamente difícil encontrar esses tipos de bugs.
Wolfgang Bangerth 18/09/19