Como manipular dividir por zero em um idioma que não suporta exceções?

61

Estou no meio do desenvolvimento de uma nova linguagem de programação para resolver alguns requisitos de negócios, e essa linguagem é direcionada a usuários inexperientes. Portanto, não há suporte para o tratamento de exceções no idioma, e eu não esperaria que eles o usassem mesmo se eu o adicionasse.

Cheguei ao ponto em que tenho que implementar o operador de divisão e estou querendo saber como lidar melhor com um erro de divisão por zero?

Parece que tenho apenas três maneiras possíveis para lidar com este caso.

  1. Ignore o erro e produza 0 como resultado. Registrando um aviso, se possível.
  2. Adicione NaN como um possível valor para números, mas isso levanta questões sobre como lidar com NaN valores em outras áreas da linguagem.
  3. Encerre a execução do programa e informe ao usuário um erro grave ocorrido.

A opção nº 1 parece ser a única solução razoável. A opção 3 não é prática, pois esta linguagem será usada para executar a lógica como um cron noturno.

Quais são as minhas alternativas para lidar com uma divisão por erro zero, e quais são os riscos em seguir a opção nº 1.

    
por cgTag 18.08.2013 / 13:45
fonte

16 respostas

98

Aconselho vivamente contra o n.º 1, porque apenas ignorar os erros é um anti-padrão perigoso. Pode levar a erros difíceis de analisar. Definir o resultado de uma divisão por zero a 0 não faz sentido algum, e continuar a execução do programa com um valor sem sentido vai causar problemas. Especialmente quando o programa está sendo executado sem supervisão. Quando o interpretador do programa percebe que há um erro no programa (e uma divisão por zero é quase sempre um erro de design), abortá-lo e manter tudo como está é normalmente preferível ao preencher seu banco de dados com lixo.

Além disso, você provavelmente não conseguirá seguir este padrão completamente. Mais cedo ou mais tarde, você encontrará situações de erro que não podem ser ignoradas (como ficar sem memória ou um estouro de pilha) e você terá que implementar uma maneira de terminar o programa de qualquer maneira.

A opção # 2 (usando NaN) seria um pouco trabalhosa, mas não tanto quanto você imagina. Como lidar com NaN em cálculos diferentes é bem documentado no padrão IEEE 754, então você pode fazer o que a linguagem em que seu interpretador está escrito faz.

A propósito: A criação de uma linguagem de programação utilizável por não-programadores é algo que estamos tentando fazer desde 1964 (Dartmouth BASIC). Até agora, não tivemos sucesso. Mas boa sorte, de qualquer maneira.

    
por 18.08.2013 / 14:48
fonte
33

1 - Ignore the error and produce 0 as the result. Logging a warning if possible.

Isso não é uma boa ideia. Em absoluto. As pessoas começarão a depender disso e, caso você conserte isso, você quebrará muito código.

2 - Add NaN as a possible value for numbers, but that raises questions about how to handle NaN values in other areas of the language.

Você deve lidar com NaN da mesma forma que os tempos de execução de outras linguagens: Qualquer cálculo adicional também gera NaN e todas as comparações (mesmo NaN == NaN) produzem resultados falsos.

Eu acho que isso é aceitável, mas não necessariamente novo amigável.

3 - Terminate the execution of the program and report to the user a severe error occurred.

Esta é a melhor solução que eu acho. Com essa informação em mãos, os usuários devem ser capazes de manipular 0. Você deve fornecer um ambiente de teste, especialmente se ele for executado uma vez por noite.

Há também uma quarta opção. Faça da divisão uma operação ternária. Qualquer um desses dois funcionará:

  • div (numerador, denumerador, alternativo_resultado)
  • div (numerador, denumerador, alternative_denumerator)
por 18.08.2013 / 15:43
fonte
20

Encerre o aplicativo em execução com extremo preconceito. (Enquanto fornece informações de depuração adequadas)

Em seguida, eduque seus usuários para identificar e manipular condições em que o divisor pode ser zero (valores inseridos pelo usuário, etc.)

    
por 18.08.2013 / 15:23
fonte
12

No Haskell (e similar no Scala), em vez de lançar exceções (ou retornar referências nulas), os tipos de wrapper Maybe e Either podem ser usados. Com Maybe , o usuário tem a chance de testar se o valor obtido está "vazio" ou pode fornecer um valor padrão ao "desembrulhar". Either é semelhante, mas pode ser usado para retornar um objeto (por exemplo, uma string de erro) descrevendo o problema, se houver um.

    
por 18.08.2013 / 18:34
fonte
12

Outras respostas já consideraram os méritos relativos de suas ideias. Proponho uma outra: use análise básica de fluxo para determinar se uma variável pode ser zero. Então você pode simplesmente proibir a divisão por variáveis potencialmente zero.

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.

Como alternativa, tenha uma função de afirmação inteligente que estabeleça invariantes:

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

Isso é tão bom quanto lançar um erro de tempo de execução - você contorna completamente as operações indefinidas - mas tem a vantagem de que o caminho do código nem precisa ser atingido pela possível falha a ser exposta. Isso pode ser feito de maneira muito semelhante ao typechecking comum, avaliando todas as ramificações de um programa com ambientes de digitação aninhados para rastreamento e verificação 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

Além disso, ele se estende naturalmente ao intervalo e à null checking, se o seu idioma tiver esses recursos.

    
por 18.08.2013 / 23:23
fonte
10

O número 1 (inserir zero não desconectável) é sempre ruim. A escolha entre # 2 (propagar NaN) e # 3 (matar o processo) depende do contexto e idealmente deve ser uma configuração global, como é em Numpy.

Se você está fazendo um cálculo grande e integrado, propagar o NaN é uma má ideia, porque ele acabará se espalhando e infectará todo o seu cálculo --- quando você olha os resultados de manhã e vê que eles são todos NaN , você teria que jogar fora os resultados e começar de novo de qualquer maneira. Teria sido melhor se o programa terminasse, você recebesse uma ligação no meio da noite e a consertasse - em termos do número de horas perdidas, pelo menos.

Se você estiver fazendo muitos cálculos pouco independentes (como cálculos com map-reduce ou embaraçosamente paralelos), e você pode tolerar que alguns deles sejam inutilizáveis devido a NaNs, provavelmente essa é a melhor opção. Terminar o programa e não fazer os 99% que seriam bons e úteis por causa dos 1% que são malformados e divididos por zero pode ser um erro.

Outra opção, relacionada a NaNs: a mesma especificação de ponto flutuante IEEE define Inf e -Inf, e estes são propagados de maneira diferente de NaN. Por exemplo, tenho certeza de que Inf > qualquer número e -Inf < qualquer número, que seria o que você queria se sua divisão por zero acontecesse porque o zero deveria ser apenas um pequeno número. Se suas entradas forem arredondadas e sofrerem de erros de medição (como medições físicas feitas à mão), a diferença de duas grandes quantidades pode resultar em zero. Sem a divisão por zero, você teria obtido um grande número, e talvez não se importe com o tamanho. Nesse caso, In e -Inf são resultados perfeitamente válidos.

Também pode ser formalmente correto - apenas diga que você está trabalhando nos reais estendidos.

    
por 18.08.2013 / 22:33
fonte
8

3. Terminate the execution of the program and report to the user a severe error occurred.

[This option] is not practical…

Claro que é prático: É responsabilidade dos programadores escrever um programa que realmente faça sentido. Dividir por 0 não faz sentido. Portanto, se o programador está executando uma divisão, também é sua responsabilidade verificar antecipadamente que o divisor não é igual a 0. Se o programador não executar essa verificação de validação, então ele / ela deve perceber que o erro o mais rápido possível, e resultados de computação desnormalizados (NaN) ou incorretos (0) simplesmente não ajudarão nesse aspecto.

A opção 3 é a que eu recomendaria a você, por ser a mais direta, honesta e matematicamente correta.

    
por 19.08.2013 / 11:11
fonte
4

Parece-me uma má idéia executar tarefas importantes (ou seja, "nightly cron") em um ambiente onde os erros são ignorados. É uma péssima ideia fazer isso uma característica. Isto exclui as opções 1 e 2.

A opção 3 é a única solução aceitável. Exceções não precisam fazer parte da linguagem, mas são parte da realidade. Sua mensagem de encerramento deve ser o mais específica e informativa possível sobre o erro.

    
por 21.08.2013 / 21:08
fonte
3

O IEEE 754 possui uma solução bem definida para o seu problema. Manipulação de exceção sem usar exceptions link

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

Dessa forma, todas as suas operações fazem sentido matematicamente.

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

Na minha opinião, seguindo o IEEE 754, faz mais sentido, pois garante que seus cálculos sejam tão corretos quanto em um computador, e você também é consistente com o comportamento de outras linguagens de programação.

O único problema que surge é que Inf e NaN contaminam seus resultados e seus usuários não saberão exatamente de onde o problema está vindo. Dê uma olhada em uma linguagem como Julia que faz isso muito bem.

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

O erro de divisão é propagado corretamente através das operações matemáticas, mas no final o usuário não necessariamente sabe de qual operação o erro deriva.

edit: Eu não vi a segunda parte da resposta de Jim Pivarski, que é basicamente o que estou dizendo acima. Meu mal.

    
por 13.07.2014 / 09:29
fonte
2

Acho que o problema é "direcionado a usuários iniciantes. - > Portanto, não há suporte para ..."

Por que você está pensando que o tratamento de exceção é problemático para usuários iniciantes?

O que é pior? Tem um recurso "difícil" ou não tem idéia de por que algo aconteceu? O que poderia confundir mais? Uma falha com um dump principal ou "Erro fatal: dividir por zero"?

Em vez disso, acho que é MELHOR apontar para erros de mensagem EXCELENTES. Então, em vez disso: "Cálculo incorreto, Divida 0/0" (ou seja: Sempre mostrar os DADOS que causam o problema, não apenas o tipo do problema). Veja como o PostgreSql faz os erros da mensagem, que são ótimos IMHO.

No entanto, você pode ver outras formas de trabalhar com exceções como:

link

Eu também tenho o sonho de criar um idioma, e neste caso eu acho que mixar um Maybe / Optional com exceções normais poderia ser o melhor:

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')
    
por 13.07.2014 / 06:45
fonte
1

Na minha opinião, seu idioma deve fornecer um mecanismo genérico para detectar e manipular erros. Os erros de programação devem ser detectados em tempo de compilação (ou o mais cedo possível) e devem normalmente levar à finalização do programa. Erros que resultam de dados inesperados ou errados, ou de condições externas inesperadas, devem ser detectados e disponibilizados para ação apropriada, mas permitir que o programa continue sempre que possível.

Ações plausíveis incluem (a) encerrar (b) solicitar ao usuário uma ação (c) registrar o erro (d) substituir um valor corrigido (e) configurar um indicador a ser testado no código (f) invocar um tratamento de erro rotina. Quais destes você disponibiliza e de que maneira são escolhas que você deve fazer.

De acordo com minha experiência, erros de dados comuns como conversões com falhas, divisão por zero, estouro e valor fora do intervalo são benignos e devem, por padrão, ser manipulados substituindo um valor diferente e definindo um sinalizador de erro. O (não-programador) usando esta linguagem verá os dados defeituosos e entenderá rapidamente a necessidade de verificar erros e lidar com eles.

[Por exemplo, considere uma planilha do Excel. O Excel não finaliza sua planilha porque um número transbordou ou o que quer que seja. O celular recebe um valor estranho e você descobre o motivo e conserta.]

Então, para responder à sua pergunta: você certamente não deve terminar. Você pode substituir NaN, mas não deve torná-lo visível, apenas certifique-se de que o cálculo seja concluído e gere um alto valor estranho. E defina um sinalizador de erro para que os usuários que precisam dele possam determinar que ocorreu um erro.

Divulgação: criei exatamente essa implementação de linguagem (Powerflex) e resolvi exatamente esse problema (e muitos outros) nos anos 80. Houve pouco ou nenhum progresso em linguagens para não-programadores nos últimos 20 anos, e você atrairá muitas críticas por tentar, mas eu realmente espero que você tenha sucesso.

    
por 12.07.2014 / 13:18
fonte
1

O SQL, facilmente a linguagem mais usada pelos não-programadores, faz o # 3, por qualquer coisa que valha a pena. Na minha experiência de observar e auxiliar os não programadores que escrevem SQL, esse comportamento é geralmente bem compreendido e facilmente compensado (com uma declaração de caso ou algo semelhante). Isso ajuda que a mensagem de erro que você recebe tende a ser bastante direta. no Postgres 9 você recebe "ERRO: divisão por zero".

    
por 13.07.2014 / 06:28
fonte
1

Eu gostei do operador ternário onde você fornece um valor alternativo caso o denumerador seja 0.

Mais uma ideia que não vi é produzir um valor geral "inválido". Um geral "esta variável não tem um valor porque o programa fez algo ruim", que carrega consigo um rastreio de pilha completo. Então, se você usar esse valor em qualquer lugar, o resultado será novamente inválido, com a nova operação sendo tentada na parte superior (ou seja, se o valor inválido aparecer em uma expressão, a expressão inteira é inválida e nenhuma chamada de função é tentada; ser operadores booleanos - verdadeiro ou inválido é verdadeiro e falso e inválido é falso - pode haver outras exceções, também). Uma vez que esse valor não é mais referenciado em nenhum lugar, você registra uma descrição longa e agradável de toda a cadeia onde as coisas estavam erradas e continua os negócios como de costume. Talvez envie o rastreamento por e-mail para o líder do projeto ou algo assim.

Algo como o Maybe monad basicamente. Ele funcionará com qualquer outra coisa que possa falhar, e você pode permitir que as pessoas construam seus próprios inválidos. E o programa continuará rodando enquanto o erro não for muito profundo, o que é o que realmente é desejado aqui, eu acho.

    
por 13.07.2014 / 09:05
fonte
1

Existem duas razões fundamentais para uma divisão por zero.

  1. Em um modelo preciso (como inteiros), você obtém uma divisão por zero DBZ porque a entrada está errada. Este é o tipo de DBZ que a maioria de nós pensa.
  2. No modelo não preciso (como o ponto flutuante), você pode obter um DBZ devido ao erro de arredondamento, mesmo que a entrada seja válida. Isso é o que normalmente não pensamos.

Por 1. você deve comunicar aos usuários que eles cometeram um erro, porque eles são os responsáveis e eles são os que melhor sabem como remediar a situação.

Para 2. Isso não é culpa do usuário, você pode apontar dedo para algoritmo, implementação de hardware, etc, mas isso não é culpa do usuário, então você não deve encerrar o programa ou até mesmo lançar exceção (se permitido que não é neste caso ). Portanto, uma solução razoável é continuar as operações de maneira razoável.

Eu posso ver a pessoa que fez esta pergunta para o caso 1. Então você precisa se comunicar com o usuário. Usando qualquer padrão de ponto flutuante, Inf, -Inf, Nan, IEEE não se encaixa nessa situação. Estratégia fundamentalmente errada.

    
por 13.07.2014 / 10:11
fonte
0

Desative-o no idioma. Ou seja, não permitir a divisão por um número até que não seja zero, geralmente testando-o primeiro. Ou seja,

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

Como você está escrevendo uma linguagem de programação, você deve aproveitar o fato e torná-lo obrigatório incluir uma ação para o dispositivo pelo estado zero.  um < = n / c: 0 div-por-zero-ação

Sei que o que acabei de sugerir é essencialmente adicionar um 'goto' ao seu PL.

    
por 13.07.2014 / 21:02
fonte