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


62

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 iniciantes. Portanto, não há suporte para o tratamento de exceções no idioma, e eu não esperaria que eles o usassem, mesmo que eu o adicionasse.

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

Parece que tenho apenas três maneiras possíveis de lidar com esse caso.

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

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

Quais são minhas alternativas para lidar com um erro de divisão por zero e quais são os riscos com a opção nº 1.


12
se você se adicionar suporte exceção e que o usuário não pegá-lo, então você teria a opção # 3
catraca aberração

82
Estou curioso, que tipo de requisito estúpido exigiria que você criasse uma nova linguagem de programação? Na minha experiência, todas as linguagens já criadas são péssimas (em design ou em execução, geralmente em ambas) e foi preciso muito esforço para conseguir isso . Há algumas exceções para o primeiro, mas não para a segunda, e como eles são facilmente <0,01% dos casos, eles são erros provavelmente medição ;-)

16
@delnan a maioria dos novos idiomas é criada para permitir que as regras de negócios sejam separadas de como elas são implementadas. O usuário não precisa saber como reject "Foo"foi implementado, mas simplesmente que rejeita um documento se ele contiver a palavra-chave Foo. Tento facilitar a leitura do idioma usando termos com os quais o usuário está familiarizado. Fornecer ao usuário sua própria linguagem de programação permite que ele adicione regras de negócios sem depender da equipe técnica.
Reactgular

19
@Mathew Foscarini. Nunca, jamais, ignore o erro e silenciosamente retorne 0. Ao fazer uma divisão, 0 pode ser um valor perfeitamente legal (por algum motivo, existe algo no Power Basic e é realmente uma dor). Se você dividir números de ponto flutuante, Nan ou Inf seria bom (dê uma olhada no IEEE 754 para entender o porquê). Se você dividir números inteiros, poderá interromper o programa; nunca deve ser permitido dividir por 0 (bem, a menos que você queira implementar um sistema de exceções verdadeiras).

16
Diverti-me e fascinei-me por um domínio de negócio complexo o suficiente para justificar uma linguagem de programação proprietária completa de Turing, mas relaxado o suficiente para tolerar resultados drasticamente imprecisos.
Mark E. Haase

Respostas:


98

Eu recomendaria fortemente contra o nº 1, porque apenas ignorar erros é um perigoso anti-padrão. Isso pode levar a erros difíceis de analisar. Definir o resultado de uma divisão por zero como 0 não faz sentido algum, e a execução contínua do programa com um valor sem sentido vai causar problemas. Especialmente quando o programa está sendo executado sem supervisão. Quando o intérprete do programa percebe que há um erro no programa (e uma divisão por zero é quase sempre um erro de design), anulá-lo e manter tudo como está é geralmente preferido em vez de encher seu banco de dados com lixo.

Além disso, é improvável que você tenha sucesso ao seguir completamente esse padrão. Mais cedo ou mais tarde, você encontrará situações de erro que simplesmente não podem ser ignoradas (como falta de memória ou estouro de pilha) e você precisará implementar uma maneira de finalizar o programa de qualquer maneira.

A opção 2 (usando NaN) seria um pouco de trabalho, mas não tanto quanto você imagina. Como lidar com o NaN em cálculos diferentes está bem documentado no padrão IEEE 754, portanto você provavelmente pode fazer o que o idioma em que o seu intérprete está escrito faz.

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


14
+1 obrigado. Você me convenceu a cometer um erro e agora que li sua resposta não entendo por que estava hesitando. PHPtem sido uma má influência para mim.
Reactgular

24
Sim, tem. Quando li sua pergunta, imediatamente pensei que era algo muito semelhante ao PHP produzir uma saída incorreta e continuar transportando caminhões diante de erros. Existem boas razões pelas quais o PHP é a exceção ao fazer isso.
Joel

4
+1 para o comentário BASIC. Eu não aconselho usar NaNno idioma de um iniciante, mas em geral, ótima resposta.
quer

8
@Joel Se ele tivesse vivido o suficiente, Dijkstra provavelmente teria dito "O uso do [PHP] prejudica a mente; seu ensino deve, portanto, ser considerado um crime".
Ross Patterson

12
@Ross. "arrogância na ciência da computação é medida em nano-Dijkstras" - Alan Kay

33

1 - Ignore o erro e produza 0como resultado. Registrando um aviso, se possível.

Isso não é uma boa idéia. Em absoluto. As pessoas começarão dependendo disso e, se você precisar corrigi-lo, quebrará muito código.

2 - Adicione NaNcomo um valor possível para números, mas isso levanta questões sobre como lidar com NaNvalores em outras áreas do idioma.

Você deve manipular o NaN da mesma forma que os tempos de execução de outros idiomas: qualquer cálculo adicional também produz NaN e todas as comparações (mesmo NaN == NaN) produzem false.

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

3 - Finalize a execução do programa e relate ao usuário que ocorreu um erro grave.

Esta é a melhor solução que eu acho. Com essas informações em mãos, os usuários devem ser capazes de lidar com 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, resultado_ alternativo)
  • div (numerador, denumerador, alternativa_denumerador)

Mas se você fizer NaN == NaNser false, então você terá que adicionar uma isNaN()função para que os usuários são capazes de detectar NaNs.
AJMansfield

2
@AJMansfield: Ou isso, ou as pessoas implementá-lo eles mesmos: isNan(x) => x != x. Ainda assim, quando você NaNaparecer em seu código de programação, não deve começar a adicionar isNaNverificações, mas rastrear a causa e fazer as verificações necessárias lá. Portanto, é importante para NaNpropagar completamente.
back2dos

5
NaNs são majoritariamente contra-intuitivos. No idioma de um iniciante, eles estão mortos na chegada.
Ross Patterson

2
@ RossPatterson Mas um iniciante pode dizer facilmente 1/0- você precisa fazer algo com isso. Não há resultado possivelmente útil além de Infou NaN- algo que propague o erro ainda mais no programa. Caso contrário, a única solução é parar com um erro neste momento.
Mark Hurd

11
A opção 4 pode ser aprimorada permitindo a invocação de uma função, que por sua vez pode executar qualquer ação necessária para recuperar do inesperado divisor 0.
CyberFonic

21

Encerre o aplicativo em execução com extremo prejuízo. (Ao fornecer informações de depuração adequadas)

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


13

No Haskell (e similar no Scala), em vez de lançar exceções (ou retornar referências nulas), o wrapper digita Maybee Eitherpode ser usado. Com Maybeo usuário, é possível testar se o valor obtido é "vazio" ou ele pode fornecer um valor padrão ao "desembrulhar". Eitheré semelhante, mas pode ser usado retorna um objeto (por exemplo, uma string de erro) descrevendo o problema, se houver algum.


11
É verdade, mas observe que Haskell não usa isso para divisão por zero. Em vez disso, todo tipo de Haskell tem implicitamente "bottom" como um valor possível. Isso não é como ponteiros nulos no sentido de que é o "valor" de uma expressão que falha ao finalizar. Você não pode testar a não-terminação como um valor, é claro, mas na semântica operacional os casos que não terminam fazem parte do significado de uma expressão. Em Haskell, esse valor "inferior" também lida com resultados adicionais de casos de erro, como a error "some message"função que está sendo avaliada.
Steve314

Pessoalmente, se o efeito de abortar todo o programa é considerado válido, não sei por que o código puro não pode ter o efeito de gerar uma exceção, mas sou apenas eu - Haskellnão permite que expressões puras gerem exceções.
Steve314

Eu acho que é uma boa ideia, pois, além de lançar uma exceção, todas as opções propostas não comunicam aos usuários que eles cometeram um erro. A idéia básica é que o usuário cometa um erro com o valor que atribuiu ao programa; portanto, o programa deve informar ao usuário que deu entrada incorreta (então o usuário pode pensar em uma maneira de remediar). Sem contar aos usuários sobre seus erros, qualquer solução parece tão estranha.
InformedA

Eu acho que esse é o caminho a seguir ... A linguagem de programação Rust a usa extensivamente em sua biblioteca padrão.
aochagavia

12

Outras respostas já consideraram os méritos relativos de suas idéias. Proponho outro: use a análise básica de fluxo para determinar se uma variável pode ser zero. Então você pode simplesmente não permitir a divisão por variáveis ​​que são 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 asserção inteligente que estabelece 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 gerar um erro de tempo de execução - você evita completamente operações indefinidas - mas tem a vantagem de que o caminho do código nem precisa ser atingido para que a falha em potencial seja exposta. Isso pode ser feito da mesma maneira que as datilografias comuns, avaliando todas as ramificações de um programa com ambientes de digitação aninhados para rastrear e verificar os 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 alcance e nullverificação, se o seu idioma tiver esses recursos.


4
Boa ideia, mas esse tipo de solução de restrições é NP-completo. Imagine algo como def foo(a,b): return a / ord(sha1(b)[0]). O analisador estático não pode inverter o SHA-1. O Clang tem esse tipo de análise estática e é ótimo para encontrar erros rasos, mas há muitos casos que ele não consegue lidar.
Mark E. Haase

9
isso não é NP-completo, isso é impossível - diz interromper o lema. No entanto, o analisador estático não precisa resolver isso, ele pode simplesmente criar uma declaração como essa e exigir que você adicione uma asserção ou decoração explícita.
MK01

11
@ MK01: Em outras palavras, a análise é "conservadora".
21414 Jon Purdy

11

O número 1 (insira o zero que não pode ser carregado) é sempre ruim. A escolha entre # 2 (propagar NaN) e # 3 (finalizar o processo) depende do contexto e, idealmente, deve ser uma configuração global, como é o Numpy.

Se você estiver fazendo um cálculo grande e integrado, propagar o NaN é uma péssima idéia, porque eventualmente se espalhará e infectará todo o cálculo - quando você olha para os resultados pela manhã e vê que eles são todos NaN, você ' teria que jogar fora os resultados e começar de qualquer maneira. Teria sido melhor se o programa terminasse, você recebesse uma ligação no meio da noite e a corrigisse - em termos de número de horas perdidas, pelo menos.

Se você está fazendo muitos cálculos pouco independentes (como cálculos de redução de mapa ou embaraçosamente paralelos) e pode tolerar que uma porcentagem deles seja inutilizável devido aos NaNs, provavelmente essa é a melhor opção. Encerrar o programa e não fazer os 99% que seriam bons e úteis por causa dos 1% malformados e divididos por zero pode ser um erro.

Outra opção, relacionada aos NaNs: a mesma especificação de ponto flutuante IEEE define Inf e -Inf, e eles são propagados de maneira diferente do 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 era apenas um número pequeno. Se suas entradas são arredondadas e sofrem de erro de medição (como medições físicas feitas manualmente), a diferença de duas grandes quantidades pode resultar em zero. Sem a divisão por zero, você teria conseguido um número grande e talvez não se importe com o tamanho. Nesse caso, In e -Inf são resultados perfeitamente válidos.

Também pode estar formalmente correto - basta dizer que você está trabalhando com reais estendidos.


Mas não podemos dizer se o denominador se destina a ser positivo ou negativo; portanto, a divisão pode gerar + inf quando -inf for desejado, ou vice-versa.
Daniel Lubarov

É verdade que seu erro de medição é muito pequeno para distinguir entre + inf e -inf. Isso se assemelha mais à esfera de Riemann, na qual todo o plano complexo é mapeado para uma bola com exatamente um ponto infinito (o ponto diametralmente oposto à origem). Números positivos muito grandes, números negativos muito grandes e até números imaginários e complexos muito grandes estão todos próximos desse ponto infinito. Com um pequeno erro de medição, você não pode distingui-los.
Jim Pivarski

Se você estiver trabalhando nesse tipo de sistema, será necessário identificar + inf e -inf como equivalentes, assim como identificar +0 e -0 como equivalentes, mesmo que eles tenham representações binárias diferentes.
Jim Pivarski

8

3. Finalize a execução do programa e relate ao usuário que ocorreu um erro grave.

[Esta opção] não é prático…

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 estiver executando uma divisão, também é sua responsabilidade verificar previamente se o divisor não é igual a 0. Se o programador não executar essa verificação de validação, deverá perceber esse erro assim que possível, e resultados de computação desnormalizados (NaN) ou incorretos (0) simplesmente não ajudarão nesse sentido.

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


4

Parece-me uma má idéia executar tarefas importantes (por exemplo, "cron noturno") em um ambiente em que os erros são ignorados. É uma péssima ideia tornar isso um recurso. Isso exclui as opções 1 e 2.

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


3

O IEEE 754 realmente tem uma solução bem definida para o seu problema. Tratamento de exceção sem usar exceptions http://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling

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

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

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

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

O único problema que surge é que Inf e NaN contaminarão 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 sabe necessariamente de qual operação o erro ocorre.

edit:Não vi a segunda parte da resposta de Jim Pivarski, que é basicamente o que estou dizendo acima. Minha culpa.


2

O SQL, facilmente a linguagem mais usada por não programadores, faz o número 3, pelo que vale a pena. Na minha experiência observando e ajudando não programadores a escrever SQL, esse comportamento geralmente é bem compreendido e facilmente compensado (com uma declaração de caso ou algo semelhante). Ajuda que a mensagem de erro recebida tenda a ser bastante direta, por exemplo, no Postgres 9, você recebe "ERRO: divisão por zero".


2

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

Por que você acha que o tratamento de exceções é problemático para usuários iniciantes?

O que é pior? Tem um recurso "difícil" ou não sabe 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 é MUITO melhor procurar GRANDES erros de mensagem. Em vez disso, faça o seguinte: "Cálculo incorreto, divida 0/0" (ou seja: sempre mostre os DADOS que causam o problema, não apenas o tipo de problema). Veja como o PostgreSql faz os erros da mensagem, que são ótimos IMHO.

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

http://dlang.org/exception-safe.html

Eu também tenho o sonho de construir uma linguagem e, nesse caso, acho que misturar um Talvez / Opcional 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')

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 no momento da compilação (ou o mais cedo possível) e normalmente devem levar ao encerramento do programa. Os erros resultantes de dados inesperados ou errôneos ou de condições externas inesperadas devem ser detectados e disponibilizados para a ação apropriada, mas permitem que o programa continue sempre que possível.

As 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) definir um indicador a ser testado no código (f) invocar uma rotina de tratamento de erros. Qual destes você disponibiliza e de que forma são as escolhas que precisa fazer.

De acordo com minha experiência, erros comuns de dados, como conversões defeituosas, divisão por zero, excesso e valor fora do intervalo, são benignos e, por padrão, devem ser tratados substituindo um valor diferente e definindo um sinalizador de erro. O (não programador) usando essa linguagem verá os dados com defeito e entenderá rapidamente a necessidade de verificar se há erros e resolvê-los.

[Por exemplo, considere uma planilha do Excel. O Excel não encerra sua planilha porque um número excedeu o limite ou o que for. A célula obtém um valor estranho e você descobre o porquê e o corrige.]

Então, para responder à sua pergunta: você certamente não deve terminar. Você pode substituir o NaN, mas não deve torná-lo visível, apenas certifique-se de que o cálculo seja concluído e gere um valor alto 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 uma implementação dessa linguagem (Powerflex) e resolvi exatamente esse problema (e muitos outros) na década de 1980. Houve pouco ou nenhum progresso em idiomas para não-programadores nos últimos 20 anos, e você atrairá muitas críticas por tentar, mas eu realmente espero que você tenha sucesso.


1

Gostei do operador ternário, no qual você fornece um valor alternativo, caso o denumerador seja 0.

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

Algo como a mônada Talvez, basicamente. Também 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á sendo executado enquanto o erro não for muito profundo, o que é realmente desejado aqui, eu acho.


1

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

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

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

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

Eu posso ver a pessoa que fez essa pergunta no caso 1. Então você precisa se comunicar novamente 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.


0

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

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero

Para fazer isso, você precisa de um novo tipo numérico, um número natural, em oposição a um número inteiro. Isso pode ser ... difícil ... de lidar.
Servy

@ Servy: Não, você não faria. Por que você? Você precisa de lógica no compilador para determinar os possíveis valores, mas deseja isso de qualquer maneira (por motivos de otimização).
MSalters

Se você não tiver um tipo diferente, um para zero e outro para valores diferentes de zero, não poderá resolver o problema no caso geral. Você teria falsos positivos e forçaria o usuário a verificar zero com mais frequência do que deveria ou criará situações em que ele ainda pode ser dividido por zero.
Servy

@ Servy: Você está enganado: um compilador pode rastrear esse estado sem precisar desse tipo e, por exemplo, o GCC já o faz. Por exemplo, o tipo C intpermite valores zero, mas o GCC ainda pode determinar onde no código as entradas específicas não podem ser zero.
MSalters

2
Mas apenas em certos casos; não é possível, com 100% de precisão, em todos os casos. Você terá falsos positivos ou falsos negativos. Isto é comprovadamente verdade. Por exemplo, eu poderia criar um trecho de código que pode ou não ser concluído . Se o compilador não pode nem saber se foi concluído, como poderia saber se o int resultante é diferente de zero? Ele pode capturar casos óbvios simples, mas nem todos .
Servy

0

Ao escrever uma linguagem de programação, você deve aproveitar o fato e tornar obrigatório incluir uma ação para o dispositivo por estado zero. a <= n / c: 0 div por ação zero

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.