Maneira mais limpa de relatar erros no Haskell

22

Estou trabalhando no aprendizado do Haskell e me deparei com três maneiras diferentes de lidar com erros nas funções que escrevo:

  1. Eu posso simplesmente escrever error "Some error message.", o que gera uma exceção.
  2. Posso retornar minha função Maybe SomeType, onde posso ou não ser capaz de retornar o que gostaria de retornar.
  3. Posso ter minha função retornada Either String SomeType, onde posso retornar uma mensagem de erro ou o que me foi pedido para retornar em primeiro lugar.

Minha pergunta é: Qual método de tratamento de erros devo usar e por quê? Talvez eu deva métodos diferentes, dependendo do contexto?

Meu entendimento atual é:

  • É "difícil" lidar com exceções no código puramente funcional, e em Haskell queremos manter as coisas o mais puramente funcionais possível.
  • Retornar Maybe SomeTypeé a coisa certa a fazer se a função falhar ou for bem-sucedida (ou seja, não há maneiras diferentes de falhar ).
  • Retornar Either String SomeTypeé a coisa certa a fazer se uma função pode falhar de várias maneiras.
CmdrMoozy
fonte

Respostas:

32

Tudo bem, primeira regra de tratamento de erros no Haskell: nunca useerror .

É simplesmente terrível em todos os sentidos. Ele existe puramente como um ato histórico e o fato de o Prelude usá-lo é terrível. Não use.

O único momento concebível que você pode usar é quando algo é tão terrível internamente que algo deve estar errado com o próprio tecido da realidade, tornando assim o resultado do seu programa discutível.

Agora, a questão torna-se Maybevs Either. Maybeé adequado para algo como head, que pode ou não retornar um valor, mas há apenas uma razão possível para falhar. Nothingestá dizendo algo como "quebrou e você já sabe o porquê". Alguns diriam que indica uma função parcial.

A forma mais robusta de tratamento de erros é Either+ um erro ADT.

Por exemplo, em um dos meus compiladores de hobby, tenho algo como

data CompilerError = ParserError ParserError
                   | TCError TCError
                   ...
                   | ImpossibleError String

data ParserError = ParserError (Int, Int) String
data TCError = CouldntUnify Ty Ty
             | MissingDefinition Name
             | InfiniteType Ty
             ...

type ErrorM m = ExceptT CompilerError m -- from MTL

Agora, defino vários tipos de erros, aninhando-os para que eu tenha um glorioso erro de nível superior. Isso pode ser um erro de qualquer estágio da compilação ou de umImpossibleError , o que significa um erro do compilador.

Cada um desses tipos de erro tenta manter o máximo de informações pelo maior tempo possível para uma impressão bonita ou outra análise. Mais importante, por não ter uma string, posso testar se a execução de um programa digitado por meio do verificador de tipos realmente gera um erro de unificação! Quando algo é um String, desapareceu para sempre e qualquer informação contida é opaca para o compilador / testes, então Either Stringtambém não é ótimo.

Finalmente, eu embalo esse tipo em ExceptTum novo transformador de mônada da MTL. Isto é essencialmenteEitherT e vem com um bom lote de funções para gerar e capturar erros de uma maneira pura e agradável.

Por fim, vale mencionar que Haskell tem os mecanismos para oferecer suporte ao tratamento de exceções, como outras linguagens, exceto que a captura de uma exceção vive IO. Sei que algumas pessoas gostam de usá-las para IOaplicativos pesados, onde tudo pode potencialmente falhar, mas com tanta frequência que não gostam de pensar nisso. Se você usa essas exceções impuras ou apenas ExceptT Error IOé realmente uma questão de gosto. Pessoalmente, opto ExceptTporque gosto de ser lembrado da chance de fracasso.


Como resumo,

  • Maybe - Eu posso falhar de uma maneira óbvia
  • Either CustomType - Eu posso falhar, e eu vou te contar o que aconteceu
  • IO+ exceções - às vezes falho. Verifique meus documentos para ver o que eu jogo quando faço
  • error - Eu também te odeio usuário
Daniel Gratzer
fonte
Esta resposta esclarece muito para mim. Eu estava me adivinhando com base no fato de que funções internas gostam headou lastparecem usar error, então fiquei pensando se essa seria realmente uma boa maneira de fazer as coisas e que estava faltando alguma coisa. Isso responde a essa pergunta. :)
CmdrMoozy
5
@CmdrMoozy Fico feliz em ajudar :) É realmente lamentável que o prelúdio tenha práticas tão ruins. Esse é o caminho do legado: /
Daniel Gratzer
3
+1 Seu resumo é incrível
recursion.ninja
2

Eu não separaria 2 e 3 com base em quantas maneiras algo pode falhar. Assim que você achar que há apenas uma maneira possível, outra mostrará sua face. A métrica deveria ser "meus chamadores se importam por que algo falhou? Eles podem realmente fazer algo sobre isso?".

Além disso, não está imediatamente claro para mim que Either String SomeType pode produzir uma condição de erro. Eu faria um tipo de dados algébrico simples com as condições com um nome mais descritivo.

O uso que você usa depende da natureza do problema que você enfrenta e das expressões idiomáticas do pacote de software com o qual você está trabalhando. Embora eu tenderia a evitar o # 1.

Telastyn
fonte
Na verdade, usar Eithererros é um padrão muito conhecido no Haskell.
precisa saber é o seguinte
1
@rufflewind - Eithernão é a parte pouco clara, é o uso de Stringum erro e não uma string real.
Telastyn 12/08
Ah, eu interpretei mal sua intenção.
precisa saber é o seguinte