Por que projetar uma linguagem moderna sem um mecanismo de tratamento de exceções?

44

Muitos idiomas modernos fornecem excelentes recursos de manipulação de exceção , mas a linguagem de programação Swift da Apple não fornece um mecanismo de tratamento de exceções .

Mergulhado em exceções como estou, estou tendo problemas para entender o que isso significa. O Swift tem asserções e, claro, valores de retorno; mas estou tendo problemas para imaginar como meu modo de pensar baseado em exceção é mapeado para um mundo sem exceções (e, por esse motivo, por que tal mundo é desejável ). Há coisas que não posso fazer em uma linguagem como a Swift que eu poderia fazer com exceções? Eu ganho alguma coisa perdendo exceções?

Como, por exemplo, posso expressar melhor algo como

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

em um idioma (Swift, por exemplo) que não possui tratamento de exceções?

    
por orome 03.10.2014 / 20:47
fonte

8 respostas

33

Na programação embutida, as exceções tradicionalmente não eram permitidas, porque a sobrecarga do desenrolar da pilha que você precisa fazer era considerada uma variação inaceitável ao tentar manter o desempenho em tempo real. Embora smartphones possam tecnicamente ser considerados plataformas em tempo real, eles são poderosos o suficiente agora, onde as antigas limitações dos sistemas embarcados não se aplicam mais. Eu apenas trago isso por uma questão de meticulosidade.

As exceções geralmente são suportadas em linguagens de programação funcionais, mas tão raramente usadas que podem não ser. Um motivo é a avaliação lenta, que é feita ocasionalmente, mesmo em idiomas que não são preguiçosos por padrão. Ter uma função que é executada com uma pilha diferente do local em que ela foi enfileirada para executar dificulta determinar onde colocar seu manipulador de exceções.

O outro motivo é que as funções de primeira classe permitem construções como opções e futuros que oferecem a você os benefícios sintáticos das exceções com mais flexibilidade. Em outras palavras, o restante da linguagem é expressivo o suficiente para que as exceções não lhe comprem nada.

Não estou familiarizado com o Swift, mas o pouco que li sobre seu tratamento de erros sugere que eles pretendiam que o tratamento de erros seguisse padrões mais funcionais. Eu vi exemplos de código com success e failure blocos que se parecem muito com futuros.

Veja um exemplo usando um Future de este tutorial do Scala :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Você pode ver que tem aproximadamente a mesma estrutura do seu exemplo usando exceções. O bloco future é como um try . O bloco onFailure é como um manipulador de exceções. No Scala, como nas linguagens mais funcionais, Future é implementado completamente usando a própria linguagem. Não requer nenhuma sintaxe especial como as exceções. Isso significa que você pode definir suas próprias construções semelhantes. Talvez adicione um bloco timeout , por exemplo, ou a funcionalidade de registro.

Além disso, você pode passar o futuro, devolvê-lo da função, armazená-lo em uma estrutura de dados ou qualquer outra coisa. É um valor de primeira classe. Você não está limitado como exceções que devem ser propagadas diretamente na pilha.

As opções resolvem o problema de tratamento de erros de uma maneira ligeiramente diferente, o que funciona melhor em alguns casos de uso. Você não está preso apenas com o método.

Esse é o tipo de coisa que você "ganha ao perder exceções".

    
por 03.10.2014 / 23:00
fonte
18

Exceções podem tornar o código mais difícil de raciocinar. Embora não sejam tão poderosas quanto as gotos, elas podem causar muitos dos mesmos problemas devido à sua natureza não local. Por exemplo, digamos que você tenha um código imperativo como este:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

Você não pode dizer imediatamente se algum desses procedimentos pode lançar uma exceção. Você tem que ler a documentação de cada um desses procedimentos para descobrir isso. (Algumas linguagens facilitam isso aumentando o tipo de assinatura com essas informações.) O código acima irá compilar muito bem, independentemente de qualquer um dos procedimentos ser lançado, tornando realmente fácil esquecer de lidar com uma exceção.

Além disso, mesmo que a intenção seja propagar a exceção de volta ao chamador, geralmente é necessário adicionar código adicional para impedir que as coisas sejam deixadas em um estado inconsistente (por exemplo, se a cafeteira quebrar, você ainda precisará limpar a exceção). bagunça e retorne a caneca!). Assim, em muitos casos, o código que usa exceções seria tão complexo quanto o código que não era devido à limpeza extra necessária.

As exceções podem ser emuladas com um sistema de tipos suficientemente poderoso. Muitos dos idiomas que evitam exceções usam valores de retorno para obter o mesmo comportamento. É semelhante a como é feito em C, mas os sistemas de tipos modernos geralmente tornam mais elegante e também mais difícil de se esquecer de lidar com a condição de erro. Eles também podem fornecer açúcar sintático para tornar as coisas menos complicadas, às vezes quase tão limpas quanto seria com exceções.

Em particular, incorporar a manipulação de erros ao sistema de tipos, em vez de implementar como um recurso separado, permite que "exceções" sejam usadas para outras coisas que nem estão relacionadas a erros. (É bem sabido que o tratamento de exceções é, na verdade, uma propriedade das mônadas.)

    
por 04.10.2014 / 06:06
fonte
9

Há algumas ótimas respostas aqui, mas acho que uma razão importante não foi enfatizada o suficiente: Quando ocorrem exceções, os objetos podem ser deixados em estados inválidos. Se você puder "capturar" uma exceção, seu código de manipulador de exceções poderá acessar e trabalhar com esses objetos inválidos. Isso vai dar errado, a menos que o código desses objetos tenha sido escrito perfeitamente, o que é muito, muito difícil de fazer.

Por exemplo, imagine implementar o Vector. Se alguém instanciar seu Vector com um conjunto de objetos, mas ocorrer uma exceção durante a inicialização (talvez, digamos, ao copiar seus objetos para a memória recém-alocada), é muito difícil codificar corretamente a implementação Vector de tal forma que nenhum memória vazou. Este breve artigo do Stroustroup aborda o exemplo do Vetor .

E isso é apenas a ponta do iceberg. E se, por exemplo, você tivesse copiado alguns dos elementos, mas não todos os elementos? Para implementar um contêiner como Vector corretamente, você quase tem que tornar todas as ações que você tomar reversíveis, portanto, toda a operação é atômica (como uma transação de banco de dados). Isso é complicado e a maioria dos aplicativos errou. E mesmo quando isso é feito corretamente, complica muito o processo de implementação do container.

Portanto, algumas linguagens modernas decidiram que não vale a pena. O Rust, por exemplo, tem exceções, mas elas não podem ser "capturadas", portanto, não há como o código interagir com objetos em um estado inválido.

    
por 12.02.2015 / 21:31
fonte
6

Uma coisa que inicialmente fiquei surpreso com a linguagem Rust é que ela não suporta exceções de captura. Você pode lançar exceções, mas somente o tempo de execução pode capturá-las quando uma tarefa (pense em thread, mas nem sempre em um thread de sistema operacional separado) morrer; Se você iniciar uma tarefa sozinho, poderá perguntar se ela saiu normalmente ou se foifail!() ed.

Como tal, não é idiomático para fail com muita frequência. Os poucos casos em que isso acontece são, por exemplo, no arnês de teste (que não sabe como é o código do usuário), como o nível superior de um compilador (a maioria dos compiladores se baseia) ou ao chamar um retorno de chamada na entrada do usuário.

Em vez disso, o idioma comum é usar o Result template para explicitamente descarta erros que devem ser manipulados. Isso é significativamente facilitado pela macro try! , que pode ser encontrada em qualquer expressão que produz um resultado e produz o braço de sucesso se houver um, ou então retorna cedo da função.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
    
por 04.10.2014 / 06:53
fonte
5

Na minha opinião, as exceções são uma ferramenta essencial para detectar erros de código em tempo de execução. Tanto nos testes quanto na produção. Torne suas mensagens detalhadas o suficiente para que, em combinação com um rastreamento de pilha, você possa descobrir o que aconteceu em um log.

As exceções são principalmente uma ferramenta de desenvolvimento e uma maneira de obter relatórios de erros razoáveis da produção em casos inesperados.

Além da separação de interesses (caminho feliz com apenas erros esperados vs. caindo até alcançar algum manipulador genérico para erros inesperados) sendo uma coisa boa, tornando seu código mais legível e sustentável, é de fato impossível preparar seu código para todos os possíveis casos inesperados, até mesmo por inchaço com código de manipulação de erros para concluir a ilegibilidade.

Esse é realmente o significado de "inesperado".

Por falar nisso, o que é esperado e o que não é uma decisão que só pode ser tomada no site da chamada. É por isso que as exceções verificadas em Java não deram certo - a decisão é tomada no momento de desenvolver uma API, quando não está claro o que é esperado ou inesperado.

Exemplo simples: a API de um mapa de hash pode ter dois métodos:

Value get(Key)

e

Option<Value> getOption(key)

o primeiro lançando uma exceção se não for encontrado, o último dando-lhe um valor opcional. Em alguns casos, o último faz mais sentido, mas em outros, seu código deve esperar que haja um valor para uma determinada chave, portanto, se não houver um, é um erro que esse código não pode ser corrigido porque suposição falhou. Neste caso, é na verdade o comportamento desejado para sair do caminho do código e descer para algum manipulador genérico, caso a chamada falhe.

O código nunca deve tentar lidar com pressupostos básicos falhados.

Exceto marcando-os e lançando exceções bem legíveis, é claro.

Lançar exceções não é mal, mas capturá-las pode ser. Não tente corrigir erros inesperados. Pegue exceções em alguns lugares onde você deseja continuar algum loop ou operação, registre-as, talvez relate um erro desconhecido e pronto.

Blocos de captura em todo o lugar são uma péssima ideia.

Crie suas APIs de uma forma que facilite expressar sua intenção, ou seja, declarar se você espera um determinado caso, como a chave não encontrada ou não. Os usuários da sua API podem escolher a chamada de lançamento apenas para casos realmente inesperados.

Eu acho que a razão pela qual as pessoas se ressentem das exceções e vão longe demais omitindo essa ferramenta crucial para a automação do tratamento de erros e uma melhor separação de interesses de novos idiomas são experiências ruins.

Isso e alguns desentendimentos sobre o que eles realmente são bons.

Simulá-los fazendo TUDO através de ligação monádica torna seu código menos legível e sustentável, e você acaba sem um rastreamento de pilha, o que torna esta abordagem Pior.

O tratamento de erros de estilo funcional é ótimo para casos de erros esperados.

Permita que o tratamento de exceções cuide de todo o resto, para isso:)

    
por 31.05.2016 / 09:38
fonte
3

O Swift usa os mesmos princípios aqui do Objective-C, apenas mais consequentemente. No Objective-C, exceções indicam erros de programação. Eles não são manipulados, exceto pelas ferramentas de relatórios de falhas. "Manipulação de exceção" é feito fixando o código. (Existem algumas exceções. Por exemplo, nas comunicações entre processos. Mas isso é bastante raro e muitas pessoas nunca se deparam com isso. E o Objective-C realmente tem try / catch / finally / throw, mas você raramente as usa). O Swift acabou de remover a possibilidade de pegar exceções.

O Swift tem um recurso que se parece com o tratamento de exceções, mas é apenas um tratamento de erros imposto. Historicamente, o Objective-C tinha um padrão de manipulação de erros bastante generalizado: um método retornaria um BOOL (YES para sucesso) ou uma referência de objeto (nil para falha, não nulo para sucesso) e teria um parâmetro "pointer to NSError *" que seria usado para armazenar uma referência NSError. O Swift converte automaticamente as chamadas para esse método em algo parecido com o tratamento de exceções.

Em geral, as funções Swift podem facilmente retornar alternativas, como resultado se uma função funcionasse bem e um erro se falhasse; Isso faz com que o tratamento de erros seja muito mais fácil. Mas a resposta para a pergunta original: Os designers da Swift obviamente sentiram que criar uma linguagem segura e escrever código de sucesso em tal linguagem é mais fácil se a linguagem não tiver exceções.

    
por 29.08.2016 / 18:43
fonte
2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

Em C, você acabaria com algo parecido com o acima.

Are there things I can't do in Swift that I could do with exceptions?

Não, não há nada. Você acaba lidando com códigos de resultado em vez de exceções.
As exceções permitem que você reorganize seu código para que o tratamento de erros seja separado do código de caminho feliz, mas isso é tudo.

    
por 03.10.2014 / 21:27
fonte
1

Além da resposta de Charlie:

Estes exemplos de tratamento de exceções declaradas que você vê em muitos manuais e livros, parecem muito inteligentes apenas em exemplos muito pequenos.

Mesmo que você ponha de lado o argumento sobre o estado do objeto inválido, eles sempre causam muita dor quando se lida com um aplicativo grande.

Por exemplo, quando você tem que lidar com IO, usando alguma criptografia, você pode ter 20 tipos de exceções que podem ser descartadas de 50 métodos. Imagine a quantidade de código de manipulação de exceção que você precisará. O tratamento de exceção levará várias vezes mais código do que o próprio código.

Na realidade, você sabe quando a exceção não pode aparecer e você simplesmente nunca precisa gravar tanta manipulação de exceção, então você apenas usa algumas soluções alternativas para ignorar as exceções declaradas. Na minha prática, apenas cerca de 5% das exceções declaradas precisam ser tratadas no código para ter um aplicativo confiável.

    
por 28.08.2016 / 14:51
fonte