Qual é o benefício de não ter "nenhuma exceção de tempo de execução", como afirma o Elm?

16

Alguns idiomas afirmam não ter "nenhuma exceção de tempo de execução" como uma clara vantagem sobre outros idiomas que os possuem.

Estou confuso sobre o assunto.

A exceção de tempo de execução é apenas uma ferramenta, tanto quanto eu sei, e quando usada bem:

  • você pode comunicar estados "sujos" (lançando dados inesperados)
  • adicionando pilha você pode apontar para a cadeia de erro
  • você pode distinguir entre desorganização (por exemplo, retornando um valor vazio em entrada inválida) e uso inseguro que requer atenção de um desenvolvedor (por exemplo, lançando exceção em entrada inválida)
  • você pode adicionar detalhes ao seu erro com a mensagem de exceção, fornecendo mais detalhes úteis, ajudando os esforços de depuração (teoricamente)

Por outro lado, acho muito difícil depurar um software que "engole" exceções. Por exemplo

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

Portanto, a questão é: qual é a forte vantagem teórica de "não haver exceções em tempo de execução"?


Exemplo

https://guide.elm-lang.org/

Sem erros de tempo de execução na prática. Nenhum nulo. Nenhum indefinido não é uma função.

atoth
fonte
Acho que ajudaria a fornecer um exemplo ou dois dos idiomas aos quais você está se referindo e / ou links para essas reivindicações. Isso pode ser interpretado de várias maneiras.
21416 JimmyJames #
O exemplo mais recente que encontrei foi elm comparado a C, C ++, C #, Java, ECMAScript, etc. Atualizei minha pergunta @JimmyJames
atoth
2
Esta é a primeira vez que ouvi falar disso. Minha primeira reação é chamar BS. Observe as palavras de doninhas: na prática
JimmyJames
@atoth Vou editar o título da sua pergunta para torná-la mais clara, porque há várias perguntas não relacionadas que se parecem com ela (como "RuntimeException" vs "Exception" em Java). Se você não gostar do novo título, sinta-se à vontade para editá-lo novamente.
Andres F.
OK, eu queria que fosse geral o suficiente, mas posso concordar que, se isso ajudar, obrigado pela sua contribuição @AndresF.!
atoth

Respostas:

28

Exceções têm semântica extremamente limitadora. Eles devem ser tratados exatamente onde são lançados, ou na pilha de chamadas diretas para cima, e não há indicação para o programador em tempo de compilação, se você esquecer de fazê-lo.

Compare isso com Elm, onde os erros são codificados como Results ou Maybes , que são os dois valores . Isso significa que você recebe um erro do compilador se não o manipular. Você pode armazená-los em uma variável ou mesmo em uma coleção para adiar o manuseio para um horário conveniente. Você pode criar uma função para manipular os erros de uma maneira específica do aplicativo, em vez de repetir blocos try-catch muito semelhantes em todo o lugar. Você pode encadeá-los em um cálculo que tenha êxito apenas se todas as partes tiverem êxito e elas não precisarão ser amontoadas em um bloco de tentativa. Você não está limitado pela sintaxe interna.

Isso não é nada como "exceções de deglutição". Está explicitando as condições de erro no sistema de tipos e fornecendo semânticas alternativas muito mais flexíveis para lidar com elas.

Considere o seguinte exemplo. Você pode colar isso em http://elm-lang.org/try se desejar vê-lo em ação.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Observe o String.toIntna calculatefunção tem a possibilidade de falhar. Em Java, isso pode gerar uma exceção de tempo de execução. Enquanto lê a entrada do usuário, há uma chance bastante boa disso. Elm, em vez disso, me obriga a lidar com isso retornando um Result, mas observe que não preciso lidar com isso imediatamente. Eu posso duplicar a entrada e convertê-la em uma string e, em seguida , verificar se há entrada incorreta na getDefaultfunção. Esse local é muito mais adequado para a verificação do que o ponto em que ocorreu o erro ou mais na pilha de chamadas.

A maneira como o compilador força nossa mão também é muito mais refinada do que as exceções verificadas do Java. Você precisa usar uma função muito específica, como Result.withDefaultpara extrair o valor que deseja. Embora tecnicamente você possa abusar desse tipo de mecanismo, não há muito sentido. Como você pode adiar a decisão até saber uma boa mensagem de erro / padrão a ser colocada, não há motivo para não usá-la.

Karl Bielefeldt
fonte
8
That means you get a compiler error if you don't handle the error.- Bem, esse foi o raciocínio por trás das exceções verificadas em Java, mas todos sabemos o quanto isso funcionou.
Robert Harvey
4
@RobertHarvey De certa forma, as exceções verificadas de Java eram a versão do pobre homem disso. Infelizmente, eles podem ser "engolidos" (como no exemplo do OP). Eles também não eram tipos reais, tornando-os um caminho adicional estranho para o fluxo de código. Idiomas com melhores sistemas do tipo permitem erros codificar como valores de primeira classe, que é o que você faz em (digamos) Haskell com Maybe, Either, etc. Elm parece que está tomando uma página de linguagens como ML, OCaml ou Haskell.
Andres F.
3
@JimmyJames Não, eles não o forçam. Você só precisa "manipular" o erro quando desejar usar o valor. Se o fizer x = some_func(), não preciso fazer nada, a menos que queira examinar o valor de x, nesse caso, posso verificar se tenho um erro ou um valor "válido"; além disso, é um erro de tipo estático tentar usar um no lugar do outro, por isso não posso fazê-lo. Se os tipos de Elm funcionam como outras linguagens funcionais, eu posso realmente fazer coisas como compor valores de diferentes funções antes mesmo de saber se são erros ou não! Isso é típico das linguagens FP.
Andres F.
6
@atoth Mas você não tem ganhos significativos e não é uma razão muito boa (como foi explicado em múltiplas respostas à sua pergunta). Eu realmente o encorajo a aprender um idioma com uma sintaxe do tipo ML e você verá como é libertador livrar-se da sintaxe do tipo C (o ML, a propósito, foi desenvolvido no início dos anos 70, o que o torna aproximadamente um contemporâneo de C). As pessoas que desenharam este tipo de sistemas do tipo considerar este tipo de sintaxe normal, ao contrário C :) Enquanto você está nisso, não faria mal para aprender um Lisp, também :)
Andres F.
6
@ atoth Se você quiser tirar uma coisa de tudo isso, pegue esta: sempre certifique-se de não ser vítima do Blub Paradox . Não fique irritado com a nova sintaxe. Talvez seja lá por causa de características poderosas você não estiver familiarizado com :)
Andres F.
10

Para entender essa afirmação, primeiro precisamos entender o que um sistema de tipo estático nos compra. Em essência, o que um sistema de tipo estático nos oferece é uma garantia: se o tipo de programa verificar, uma certa classe de comportamentos em tempo de execução não poderá ocorrer.

Isso soa ameaçador. Bem, um verificador de tipos é semelhante a um verificador de teoremas. (Na verdade, pelo isomorfismo de Curry-Howard, eles são a mesma coisa.) Uma coisa que é muito peculiar nos teoremas é que, quando você prova um teorema, prova exatamente o que o teorema diz, nada mais. (É por exemplo, por que, quando alguém diz "Eu provei este programa correto", você deve sempre perguntar "por favor defina 'correto'".) O mesmo se aplica aos sistemas de tipos. Quando dizemos "um programa é seguro para o tipo", o que queremos dizer não é que nenhum erro possível possa ocorrer. Só podemos dizer que os erros que o sistema de tipos promete impedir não podem ocorrer.

Portanto, os programas podem ter infinitos comportamentos de tempo de execução diferentes. Desses, infinitamente muitos são úteis, mas também infinitos são "incorretos" (para várias definições de "correção"). Um sistema de tipo estático nos permite provar que um determinado conjunto finito e fixo desses infinitos comportamentos incorretos de tempo de execução não pode ocorrer.

A diferença entre sistemas de tipos diferentes é basicamente em qual, quantos e quão complexos comportamentos de tempo de execução eles podem provar não ocorrer. Sistemas de tipo fraco, como o de Java, só podem provar coisas muito básicas. Por exemplo, Java pode provar que um método digitado como retornando a Stringnão pode retornar a List. Mas, por exemplo, pode não provar que o método não não vai voltar. Também não pode provar que o método não gera uma exceção. E não pode provar que não retornará o erro errado String  - qualquer Stringum satisfará o verificador de tipos. (E, claro, mesmo nullvai satisfazê-lo também.) Há ainda coisas muito simples que Java não pode provar, que é por isso que temos exceções, como ArrayStoreException, ClassCastExceptionou todo mundo favorito, o NullPointerException.

Sistemas de tipos mais poderosos como o da Agda também podem provar coisas como "retornará a soma dos dois argumentos" ou "retorna a versão classificada da lista passada como argumento".

Agora, o que os designers de Elm querem dizer com a afirmação de que eles não têm exceções de tempo de execução é que o sistema de tipos de Elm pode provar a ausência de (uma porção significativa de) comportamentos de tempo de execução que em outros idiomas não podem ser comprovados como não ocorrendo e, portanto, podem levar comportamento errado no tempo de execução (que, na melhor das hipóteses, significa uma exceção, na pior das hipóteses significa uma falha e, na pior das hipóteses, significa nenhuma falha, nenhuma exceção e apenas um resultado silenciosamente errado).

Portanto, eles não estão dizendo "não implementamos exceções". Eles estão dizendo "coisas que seriam exceções de tempo de execução em linguagens típicas com as quais os programadores comuns que vêm para Elm teriam experiência são capturados pelo sistema de tipos". Obviamente, alguém vindo de Idris, Agda, Guru, Epigram, Isabelle / HOL, Coq ou idiomas semelhantes verá Elm como muito fraco em comparação. A declaração é mais voltada para programadores Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl, ... típicos.

Jörg W Mittag
fonte
5
Nota para possíveis editores: Sinto muito pelo uso de negativos duplos e até triplos. No entanto, eu os deixei de propósito: os sistemas de tipos garantem a ausência de certos tipos de comportamentos em tempo de execução, ou seja, eles garantem que certas coisas não ocorram. E eu queria manter intacta a formulação "que não ocorre", o que infelizmente leva a construções como "não é possível provar que um método não retornará". Se você puder encontrar uma maneira de melhorar isso, vá em frente, mas lembre-se do que foi dito acima. Obrigado!
Jörg W Mittag
2
Boa resposta no geral, mas um pequeno detalhe: "um verificador de tipos é semelhante a um provador de teoremas". Na verdade, um verificador de tipos é mais parecido com um verificador de teoremas: ambos fazem verificação , não dedução .
gardenhead
4

Elm pode garantir nenhuma exceção de tempo de execução pelo mesmo motivo C pode garantir nenhuma exceção de tempo de execução: O idioma não suporta o conceito de exceções.

Elm tem uma maneira de sinalizar condições de erro em tempo de execução, mas esse sistema não é exceções, é "Resultados". Uma função que pode falhar retorna um "Resultado" que contém um valor regular ou um erro. Elms é fortemente digitado, portanto, isso é explícito no sistema de tipos. Se uma função retornar sempre um inteiro, ele tem o tipo Int. Mas se ele retornar um número inteiro ou falhar, o tipo de retorno será Result Error Int. (A cadeia é a mensagem de erro.) Isso força você a lidar explicitamente com os dois casos no site de chamada.

Aqui está um exemplo da introdução (um pouco simplificado):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

A função toIntpode falhar se a entrada não for analisável, portanto, seu tipo de retorno é Result String int. Para obter o valor inteiro real, você precisa "descompactar" via correspondência de padrões, que por sua vez o força a lidar com os dois casos.

Resultados e exceções fundamentalmente fazem a mesma coisa, a diferença importante são os "padrões". As exceções aparecerão e encerrarão o programa por padrão, e você precisará identificá-las explicitamente se quiser manipulá-las. O resultado é o contrário - você é forçado a manipulá-los por padrão, portanto, você deve explicitamente passá-los até o topo, se quiser que eles encerrem o programa. É fácil ver como esse comportamento pode levar a um código mais robusto.

JacquesB
fonte
2
@ atoth Aqui está um exemplo. Imagine a linguagem A permite exceções. Então você recebe a função doSomeStuff(x: Int): Int. Normalmente você espera que retorne um Int, mas também pode gerar uma exceção? Sem olhar para o código fonte, você não pode saber. Por outro lado, uma linguagem B que codifica erros por meio de tipos pode ter a mesma função declarada assim: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(em Elm, esse tipo é realmente chamado Result). Ao contrário do primeiro caso, agora é imediatamente óbvio se a função pode falhar e você deve tratá-la explicitamente.
Andres F.
1
Como o @RobertHarvey implica em um comentário em outra resposta, isso parece ser basicamente como exceções verificadas em Java. O que aprendi ao trabalhar com Java nos primeiros dias em que a maioria das exceções foi verificada é que você realmente não deseja ser forçado a escrever código para sempre erros no momento em que ocorrem.
precisa saber é o seguinte
2
@JimmyJames Não é como exceções verificadas porque as exceções não compõem, podem ser ignoradas ("engolidas") e não são valores de primeira classe :) Eu realmente recomendo aprender uma linguagem funcional de tipo estatístico para realmente entender isso. Essa não é uma coisa nova que Elm inventou - é assim que você programa em linguagens como ML ou Haskell, e é diferente de Java.
Andres F.
2
@AndresF. this is how you program in languages such as ML or HaskellEm Haskell, sim; ML, não. Robert Harper, um dos principais colaboradores do Standard ML e pesquisador da linguagem de programação, considera as exceções úteis . Os tipos de erro podem atrapalhar a composição da função nos casos em que você pode garantir que um erro não ocorra. Exceções também têm desempenho diferente. Você não paga para exceções que não são lançadas, mas você paga para verificar valores de erro de cada vez, e as exceções são uma maneira natural de expressar retrocesso em alguns algoritmos
Doval
2
@ JimmyJames Espero que agora você veja que as exceções verificadas e os tipos de erro reais são apenas superficialmente similares. As exceções marcadas não se combinam normalmente, são difíceis de usar e não são orientadas a expressões (e, portanto, você pode simplesmente "engoli-las", como acontece com Java). Exceções não verificadas são menos complicadas, e é por isso que elas são a norma em todos os lugares, exceto em Java, mas são mais propensas a tropeçar em você, e você não pode dizer olhando para uma declaração de função se lança ou não, tornando seu programa mais difícil para entender.
Andres F.
2

Primeiro, observe que o seu exemplo de exceções de "deglutição" é geralmente uma prática terrível e completamente sem relação com a ausência de exceções em tempo de execução; quando você pensa sobre isso, você cometeu um erro em tempo de execução, mas optou por ocultá-lo e não fazer nada a respeito. Isso geralmente resulta em bugs difíceis de entender.

Essa pergunta pode ser interpretada de várias maneiras, mas como você mencionou Elm nos comentários, o contexto é mais claro.

Elm é, entre outras coisas, uma linguagem de programação estaticamente tipada . Um dos benefícios desse tipo de sistema de tipos é que muitas classes de erros (embora nem todas) são capturadas pelo compilador, antes que o programa seja realmente usado. Alguns tipos de erros podem ser codificados em tipos (como Elm Resulte Task), em vez de serem lançados como exceções. É isso que os projetistas do Elm querem dizer: muitos erros serão capturados no tempo de compilação em vez de no "tempo de execução", e o compilador forçará você a lidar com eles em vez de ignorá-los e esperar o melhor. Está claro por que essa é uma vantagem: é melhor que o programador perceba um problema antes que o usuário o faça.

Observe que quando você não usa exceções, os erros são codificados de outras maneiras menos surpreendentes. Da documentação de Elm :

Uma das garantias do Elm é que você não verá erros de tempo de execução na prática. NoRedInk usa Elm na produção há cerca de um ano, e eles ainda não tiveram um! Como todas as garantias do Elm, isso se resume a opções fundamentais de design de linguagem. Nesse caso, somos ajudados pelo fato de o Elm tratar erros como dados. (Você notou que fazemos muito mais dados aqui?)

Os designers de olmos são um pouco ousados ​​ao reivindicar "sem exceções de tempo de execução" , embora o qualifiquem como "na prática". O que eles provavelmente querem dizer é "menos erros inesperados do que se você estivesse codificando em javascript".

Andres F.
fonte
Estou lendo errado, ou eles estão apenas jogando um jogo semântico? Eles proíbem o nome "exceção de tempo de execução", mas apenas o substituem por um mecanismo diferente que transmite informações de erro na pilha. Parece apenas alterar a implementação de uma exceção para um conceito ou objeto semelhante que implementa a mensagem de erro de maneira diferente. Isso dificilmente destrói a terra. É como qualquer linguagem de tipo estaticamente. Compare a troca de uma exceção COM HRESULT para uma .NET. Mecanismo diferente, mas ainda é uma exceção de tempo de execução, não importa como você o chama.
Mike Suporta Monica
@ Mike Para ser sincero, não olhei para Elm em detalhes. A julgar pelos documentos, eles têm tipos Resulte Taskparecem muito semelhantes aos mais familiares Eithere Futurede outros idiomas. Diferentemente das exceções, os valores desses tipos podem ser combinados e, em algum momento, você deve explicitamente lidar com eles: eles representam um valor válido ou um erro? Eu não leio mentes, mas esta falta de surpreender o programador é provavelmente o que Elm Designers entende por "sem exceções de tempo de execução" :)
Andres F.
@ Mike, eu concordo que não é de abalar a terra. A diferença com as exceções de tempo de execução é que elas não são explícitas nos tipos (ou seja, você não pode saber, sem examinar seu código-fonte, se um trecho de código pode ser lançado); A codificação de erros nos tipos é muito explícita e impede que o programador os ignore, levando a um código mais seguro. Isso é feito por muitas linguagens FP e, de fato, não é novidade.
Andres F.
1
Conforme seus comentários, acho que há mais do que "verificações de tipo estático" . Você pode adicioná-lo ao JS usando o Typecript, que é muito menos restritivo do que um novo ecossistema de "fazer ou quebrar".
atoth
1
@AndresF .: Tecnicamente falando, o recurso mais recente do sistema de tipos Java é o Polimorfismo Paramétrico, que vem do final dos anos 60. Então, de certa forma, dizer "moderno" quando você quer dizer "não Java" é um tanto correto.
Jörg W Mittag
0

Elm afirma:

Sem erros de tempo de execução na prática. Nenhum nulo. Nenhum indefinido não é uma função.

Mas você pergunta sobre exceções de tempo de execução . Há uma diferença.

No Elm, nada retorna um resultado inesperado. NÃO é possível escrever um programa válido no Elm que produz erros de tempo de execução. Portanto, você não precisa de exceções.

Portanto, a pergunta deve ser:

Qual é o benefício de não haver "erros de tempo de execução"?

Se você puder escrever um código que nunca tenha erros de tempo de execução, seus programas nunca travarão.

Hector
fonte