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

14

Algumas linguagens afirmam ter "nenhuma exceção de tempo de execução" como uma clara vantagem sobre outras linguagens que as 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" (jogando dados inesperados)
  • adicionando pilha, você pode apontar para a cadeia de erros
  • você pode distinguir entre clutter (por exemplo, retornar um valor vazio em uma entrada inválida) e uso inseguro que precisa da atenção de um desenvolvedor (por exemplo, gerar exceção em entradas inválidas)
  • você pode adicionar detalhes ao seu erro com a mensagem de exceção, fornecendo mais detalhes úteis para ajudar nos 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
}

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

Exemplo

link

No runtime errors in practice. No null. No undefined is not a function.

    
por atoth 01.12.2016 / 17:43
fonte

5 respostas

26

As exceções têm uma semântica extremamente limitadora. Eles devem ser manipulados exatamente onde são lançados, ou na lista 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 Resultados ou Maybes , ambos valores . Isso significa que você recebe um erro do compilador se não lidar com o erro. Você pode armazená-los em uma variável ou até 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 uma computação que só é bem-sucedida se todas as suas partes forem bem-sucedidas e não precisarem ser espremidas em um único bloco try. Você não está limitado pela sintaxe interna.

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

Considere o seguinte exemplo. Você pode colá-lo no link se quiser 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 que String.toInt na função calculate tem a possibilidade de falhar. Em Java, isso tem o potencial de lançar uma exceção de tempo de execução. Ao ler a entrada do usuário, ele tem uma boa chance disso. Elm, em vez disso, me obriga a lidar com isso, retornando um Result , mas percebo que não tenho que lidar com isso imediatamente. Eu posso dobrar a entrada e convertê-la em uma string, então checar entrada ruim na função getDefault . Este local é muito mais adequado para o cheque do que o ponto onde ocorreu o erro ou para cima na pilha de chamadas.

O modo como o compilador força nossa mão também é muito mais refinado que as exceções verificadas do Java. Você precisa usar uma função muito específica como Result.withDefault para extrair o valor desejado. 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, não há motivo para não usá-la.

    
por 01.12.2016 / 20:45
fonte
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 dá, é uma garantia: iff o tipo de programa verifica, uma certa classe de comportamentos de tempo de execução não pode ocorrer.

Isso soa sinistro. Bem, um verificador de tipos é semelhante a um verificador de teoremas. (Na verdade, de acordo com o Isomorfismo de Curry-Howard, eles são a mesma coisa.) Uma coisa que é muito peculiar sobre teoremas é que quando você prova um teorema, você prova exatamente o que o teorema diz, não mais. (Isso é, por exemplo, porque, quando alguém diz "Eu provei este programa correto", você deve sempre perguntar "por favor, defina 'correto'".) O mesmo vale para sistemas de tipos. Quando dizemos "um programa é seguro para o tipo", o que queremos dizer é não que nenhum erro possível pode ocorrer. Só podemos dizer que os erros que o sistema de tipos nos promete prevenir não podem ocorrer.

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

A diferença entre sistemas de tipos diferentes é basicamente em quantos, e quantos comportamentos complexos de tempo de execução eles podem provar não ocorrer. Sistemas de tipos fracos, como o Java, só podem provar coisas muito básicas. Por exemplo, o Java pode provar que um método que é digitado como retornando um String não pode retornar um List . Mas, por exemplo, pode não provar que o método não retornará. Também não pode provar que o método não lançará uma exceção. E não pode provar que não retornará o errado String - qualquer String satisfará o verificador de tipos. (E, é claro, até null irá satisfazê-lo também.) Existem coisas muito simples que o Java não pode provar, e é por isso que temos exceções como ArrayStoreException , ClassCastException ou o favorito de todos.NullPointerException.

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

Agora, o que os projetistas da Elm querem dizer é que o sistema de tipos da Elm pode provar a ausência de (uma porção significativa de > ser provado que não ocorre e, portanto, pode levar a um comportamento errôneo em tempo de execução (o que na melhor das hipóteses significa uma exceção, em um pior caso significa uma falha e, no pior dos casos, nenhum acidente, nenhuma exceção e apenas um resultado silenciosamente errado).

Então, eles não dizem "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 típicos que vêm para Elm teriam experiência, são capturados pelo sistema de tipos". Naturalmente, alguém vindo de Idris, Agda, Guru, Epigrama, Isabelle / HOL, Coq ou linguagens semelhantes, verá Elm como muito fraco em comparação. A declaração é mais voltada para programadores típicos Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl,….

    
por 02.12.2016 / 02:43
fonte
4

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

Elm tem uma maneira de sinalizar condições de erro em tempo de execução, mas este 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 é strongmente tipado, então isso é explícito no sistema de tipos. Se uma função sempre retornar um inteiro, ela terá 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 manipular explicitamente ambos os casos no site da 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 toInt pode falhar se a entrada não for parsável, portanto, seu tipo de retorno é Result String int . Para obter o valor inteiro real, você precisa "desempacotar" via correspondência de padrões, o que, por sua vez, força você a lidar com ambos os casos.

Resultados e exceções basicamente fazem a mesma coisa, a diferença importante é o "defaults". Exceções irão aparecer e terminar o programa por padrão, e você terá que pegá-las explicitamente se quiser lidar com elas. O resultado é o contrário - você é forçado a manipulá-los por padrão, então você tem que explicitamente passá-los até o topo se quiser que eles terminem o programa. É fácil ver como esse comportamento pode levar a um código mais robusto.

    
por 01.12.2016 / 18:27
fonte
3

Primeiro, observe que seu exemplo de exceções de "engolir" é, em geral, uma prática terrível e completamente não relacionada a não ter exceções de tempo de execução; quando você pensa sobre isso, você teve um erro de tempo de execução, mas optou por ocultá-lo e não fazer nada a respeito. Isso geralmente resultará em bugs difíceis de entender.

Esta questão pode ser interpretada de várias maneiras, mas desde que você mencionou Elm nos comentários, o contexto é mais claro.

O Elm é, entre outras coisas, uma linguagem de programação digitada estaticamente . 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 Result e Task de Elm), em vez de serem lançados como exceções. Isto é o que significam os designers de Elm: muitos erros serão detectados em tempo de compilação, em vez de "tempo de execução", eo compilador forçará você a lidar com eles em vez de ignorá-los e esperar pelo melhor. É claro por que isso é uma vantagem: melhor que o programador tome conhecimento de um problema antes do usuário.

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

One of the guarantees of Elm is that you will not see runtime errors in practice. NoRedInk has been using Elm in production for about a year now, and they still have not had one! Like all guarantees in Elm, this comes down to fundamental language design choices. In this case, we are helped by the fact that Elm treats errors as data. (Have you noticed we make things data a lot here?)

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

    
por 01.12.2016 / 18:01
fonte
0

Elm afirma:

No runtime errors in practice. No null. No undefined is not a function.

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

Em Elm, nada retorna um resultado inesperado. Você NÃO pode escrever um programa válido no Elm que produz erros de tempo de execução. Assim, você não precisa de exceções.

Então, a pergunta deveria ser:

What is the benefit of having "no runtime errors"?

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

    
por 04.03.2017 / 15:54
fonte