Manière la plus propre de signaler les erreurs dans Haskell

22

Je travaille sur l'apprentissage de Haskell, et j'ai rencontré trois façons différentes de traiter les erreurs dans les fonctions que j'écris:

  1. Je peux simplement écrire error "Some error message.", ce qui lève une exception.
  2. Je peux faire revenir ma fonction Maybe SomeType, où je peux ou non être en mesure de retourner ce que je voudrais retourner.
  3. Je peux faire revenir ma fonction Either String SomeType, où je peux retourner soit un message d'erreur ou ce qu'on m'a demandé de retourner en premier lieu.

Ma question est: quelle méthode de traitement des erreurs dois-je utiliser et pourquoi? Peut-être que je devrais différentes méthodes, selon le contexte?

Ma compréhension actuelle est:

  • Il est "difficile" de traiter les exceptions dans le code purement fonctionnel, et dans Haskell, on veut garder les choses aussi purement fonctionnelles que possible.
  • Le retour Maybe SomeTypeest la bonne chose à faire si la fonction échoue ou réussit (c.-à-d. Qu'il n'y a pas différentes façons de l'échouer ).
  • Le retour Either String SomeTypeest la bonne chose à faire si une fonction peut échouer de différentes manières.
CmdrMoozy
la source

Réponses:

32

D'accord, première règle de gestion des erreurs dans Haskell: ne jamais utilisererror .

C'est juste terrible à tous points de vue. Il existe purement comme un acte d'histoire et le fait que Prelude l'utilise est terrible. Ne l'utilisez pas.

Le seul moment imaginable que vous pourriez utiliser est quand quelque chose est si terrible en interne que quelque chose doit être incorrect avec le tissu même de la réalité, rendant ainsi le résultat de votre programme théorique.

Maintenant , la question devient Maybevs Either. Maybeest bien adapté à quelque chose comme head, qui peut ou non retourner une valeur mais il n'y a qu'une seule raison possible d'échouer. Nothingdit quelque chose comme "il s'est cassé, et vous savez déjà pourquoi". Certains diraient que cela indique une fonction partielle.

La forme la plus robuste de gestion des erreurs est Either+ une erreur ADT.

Par exemple, dans l'un de mes compilateurs de loisirs, j'ai quelque chose comme

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

Maintenant, je définis un tas de types d'erreur, en les imbriquant de sorte que j'ai une glorieuse erreur de niveau supérieur. Cela peut être une erreur de n'importe quelle étape de la compilation ou un ImpossibleError, ce qui signifie un bogue du compilateur.

Chacun de ces types d'erreur essaie de conserver le plus d'informations possible le plus longtemps possible pour une jolie impression ou une autre analyse. Plus important encore, en n'ayant pas de chaîne, je peux tester que l'exécution d'un programme de type incorrect via le vérificateur de type génère en fait une erreur d'unification! Une fois que quelque chose est un String, c'est parti pour toujours et toute information qu'il contient est opaque pour le compilateur / tests, donc ce Either Stringn'est pas génial non plus.

Enfin, j'emballe ce type dans ExceptTun nouveau transformateur monade de MTL. C'est essentiellement EitherTet est livré avec un joli lot de fonctions pour lancer et attraper des erreurs de manière pure et agréable.

Enfin, il convient de mentionner que Haskell a les mécanismes pour prendre en charge la gestion des exceptions comme le font d'autres langues, sauf que la capture d'une exception existe IO. Je sais que certaines personnes aiment les utiliser pour IOdes applications lourdes où tout pourrait potentiellement échouer, mais si rarement qu'elles n'aiment pas y penser. Que vous utilisiez ces exceptions impures ou simplement, ExceptT Error IOc'est vraiment une question de goût. Personnellement, j'opte pour ExceptTcar j'aime qu'on me rappelle les risques d'échec.


En résumé,

  • Maybe - Je peux échouer d'une manière évidente
  • Either CustomType - Je peux échouer, et je vais te dire ce qui s'est passé
  • IO+ exceptions - j'échoue parfois. Consultez mes documents pour voir ce que je jette quand je le fais
  • error - Je te déteste aussi, utilisateur
Daniel Gratzer
la source
Cette réponse m'éclaircit beaucoup. Je devinais moi-même en me basant sur le fait que les fonctions intégrées aiment headou lastsemblent utiliser error, alors je me demandais si c'était vraiment une bonne façon de faire les choses et il me manquait juste quelque chose. Cela répond à cette question. :)
CmdrMoozy
5
@CmdrMoozy Heureux de vous aider :) C'est vraiment dommage que le prélude ait de si mauvaises pratiques. Telle est la voie de l'héritage: /
Daniel Gratzer
3
+1 Votre résumé est incroyable
recursion.ninja
2

Je ne séparerais pas 2 et 3 en fonction du nombre de façons dont quelque chose peut échouer. Dès que vous pensez qu'il n'y a qu'une seule façon possible, une autre montrera son visage. La métrique devrait plutôt être «mes appelants se soucient-ils de la raison pour laquelle quelque chose a échoué? Peuvent-ils réellement faire quelque chose?».

Au-delà de cela, il n'est pas immédiatement clair pour moi que cela Either String SomeTypepeut produire une condition d'erreur. Je ferais un type de données algébrique simple avec les conditions avec un nom plus descriptif.

Ce que vous utilisez dépend de la nature du problème auquel vous êtes confronté et des idiomes du progiciel avec lequel vous travaillez. Bien que j'aurais tendance à éviter le n ° 1.

Telastyn
la source
En fait, l'utilisation Eitherd'erreurs est un modèle très connu dans Haskell.
Rufflewind
1
@rufflewind - Eithern'est pas la partie peu claire, l'utilisation de Stringcomme une erreur plutôt qu'une chaîne réelle.
Telastyn
Ah, j'ai mal lu votre intention.
Rufflewind