Peut-être monade vs exceptions

Réponses:

57

Utiliser Maybe(ou son cousin Eitherqui fonctionne fondamentalement de la même manière mais vous permet de renvoyer une valeur arbitraire à la place de Nothing) a un but légèrement différent de celui des exceptions. En termes Java, c'est comme avoir une exception vérifiée plutôt qu'une exception d'exécution. Cela représente une chose à laquelle vous devez vous attendre , plutôt qu'une erreur à laquelle vous ne vous attendiez pas.

Donc, une fonction comme indexOfretournera une Maybevaleur parce que vous vous attendez à ce que l'élément ne soit pas dans la liste. Cela revient un peu à revenir nulld’une fonction, sauf d’une manière qui respecte le type, ce qui vous oblige à traiter le nullcas. Eitherfonctionne de la même manière, sauf que vous pouvez renvoyer des informations associées au cas d’erreur, c’est donc plus semblable à une exception que Maybe.

Alors, quels sont les avantages de l' approche Maybe/ Either? D'une part, c'est un citoyen de première classe de la langue. Comparons une fonction Eitherà une autre en lançant une exception. Dans le cas exceptionnel, votre seul recours réel est une try...catchdéclaration. Pour la Eitherfonction, vous pouvez utiliser les combinateurs existants pour rendre le contrôle de flux plus clair. Voici quelques exemples:

Tout d’abord, supposons que vous vouliez essayer plusieurs fonctions qui pourraient se tromper jusqu’à ce que vous n’en obteniez pas. Si vous n'en obtenez pas sans erreur, vous souhaitez renvoyer un message d'erreur spécial. C'est en fait un modèle très utile, mais ce serait une douleur horrible try...catch. Heureusement, puisqu'il ne Eithers'agit que d'une valeur normale, vous pouvez utiliser les fonctions existantes pour rendre le code beaucoup plus clair:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Un autre exemple est d'avoir une fonction optionnelle. Supposons que vous ayez plusieurs fonctions à exécuter, y compris une qui essaie d'optimiser une requête. Si cela échoue, vous voulez que tout le reste s'exécute de toute façon. Vous pourriez écrire du code comme:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Ces deux cas sont plus clairs et plus courts que l’utilisation try..catchet, plus important encore, plus sémantiques. Utiliser une fonction comme <|>ou qui optionalrend vos intentions beaucoup plus claires que d’utiliser try...catchtoujours de gérer les exceptions.

Notez également que vous n'êtes pas obligé de remplir votre code de lignes telles que if a == Nothing then Nothing else ...! Le but de traiter Maybeet en Eithertant que monade est d'éviter cela. Vous pouvez encoder la sémantique de propagation dans la fonction bind afin que vous obteniez les contrôles null / error gratuitement. Le seul moment où vous devez vérifier est explicitement si vous voulez retourner autre chose que Nothingdonné Nothing, et même alors il est facile: il y a un tas de fonctions de bibliothèque standard pour rendre plus agréable que de code.

Enfin, un autre avantage est que le type Maybe/ Eitherest simplement plus simple. Il n'est pas nécessaire d'étendre le langage avec des mots-clés ou des structures de contrôle supplémentaires: tout n'est que bibliothèque. S'agissant de valeurs normales, le système de types est simplifié: en Java, vous devez différencier les types (par exemple, le type de retour) des effets (par exemple, les throwsinstructions) que vous n'utiliseriez pas Maybe. Ils se comportent également comme n'importe quel autre type défini par l'utilisateur - il n'est pas nécessaire qu'un code spécial de traitement des erreurs soit intégré à la langue.

Un autre avantage réside dans le fait que Maybe/ Eithersont des foncteurs et des monades, ce qui signifie qu’ils peuvent tirer parti des fonctions de contrôle de flux de monades existantes (qui sont assez nombreuses) et, en général, bien jouer avec d’autres monades.

Cela dit, il y a des mises en garde. D'une part, ni Maybeni Eitherremplacer les exceptions non vérifiées. Vous voudrez un autre moyen de gérer des choses telles que la division par 0 simplement parce que ce serait difficile de voir chaque division renvoyer une Maybevaleur.

Un autre problème est le retour de plusieurs types d’erreurs (ceci ne s’applique qu’à Either). Avec des exceptions, vous pouvez placer différents types d'exceptions dans la même fonction. avec Either, vous obtenez un seul type. Cela peut être surmonté avec un sous-typage ou un ADT contenant tous les différents types d'erreurs en tant que constructeurs (cette seconde approche est celle qui est habituellement utilisée dans Haskell).

Malgré tout, je préfère l' approche Maybe/ Eitherparce que je la trouve plus simple et plus flexible.

Tikhon Jelvis
la source
Plutôt d'accord, mais je ne pense pas que l'exemple "facultatif" ait un sens - il semble que vous puissiez le faire aussi bien avec des exceptions: void Facultatif (Action act) {try {act (); } catch (Exception) {}}
Errorsatz
11
  1. Une exception peut contenir plus d'informations sur la source d'un problème. OpenFile()peut lancer FileNotFoundou NoPermissionou TooManyDescriptorsetc. A Aucun ne porte pas cette information.
  2. Les exceptions peuvent être utilisées dans des contextes dépourvus de valeurs de retour (par exemple, avec des constructeurs dans les langues qui les utilisent).
  3. Une exception vous permet d'envoyer très facilement les informations en haut de la pile, sans beaucoup d' if None return Noneinstructions de style.
  4. La gestion des exceptions a presque toujours un impact sur les performances supérieur à celui de simplement renvoyer une valeur.
  5. Plus important encore, une exception et une monade Maybe peuvent avoir des objectifs différents: une exception est utilisée pour signaler un problème, alors qu'une peut-être ne l'est pas.

    "Infirmière, s'il y a un patient dans la chambre 5, pouvez-vous lui demander d'attendre?"

    • Peut-être monade: "Docteur, il n'y a pas de patient dans la chambre 5."
    • Exception: "Docteur, il n'y a pas de place 5!"

    (notez le "si" - cela signifie que le médecin attend une monade peut-être)

Chêne
la source
7
Je ne suis pas d'accord avec les points 2 et 3. En particulier, le point 2 ne pose pas de problème dans les langues qui utilisent des monades, car ces conventions ont des conventions qui ne génèrent pas le dilemme de devoir gérer une défaillance pendant la construction. En ce qui concerne 3, les langages avec des monades ont une syntaxe appropriée pour gérer les monades de manière transparente, sans nécessiter de vérification explicite (par exemple, les Nonevaleurs peuvent simplement être propagées). Votre point 5 n’est qu’un peu correct, la question est de savoir quelles situations sont sans aucun doute exceptionnelles. Il s'avère que… pas beaucoup .
Konrad Rudolph
3
En ce qui concerne (2), vous pouvez écrire bindde manière à ce que les tests Nonene génèrent pas de surcharge syntaxique. Un exemple très simple, C # surcharge simplement les Nullableopérateurs de manière appropriée. Aucune vérification Nonenécessaire, même lorsque vous utilisez le type. Bien sûr, la vérification est toujours effectuée (c'est sûr), mais en coulisses et n'encombre pas votre code. La même chose s’applique d’une certaine manière à votre objection à mon objection à (5), mais je conviens que cela peut ne pas toujours s’appliquer.
Konrad Rudolph
4
@Oak: En ce qui concerne (3), l’intérêt de traiter Maybecomme une monade est de rendre Noneimplicite la propagation . Cela signifie que si vous voulez revenir Nonedonné None, vous n'avez pas d'écrire un code spécial du tout. La seule fois où vous devez faire correspondre, c'est si vous voulez faire quelque chose de spécial None. Vous n'avez jamais besoin de if None then Nonesortes de déclarations.
Tikhon Jelvis
5
@ Oak: c'est vrai, mais ce n'était pas vraiment ce que je voulais dire. Ce que je dis, c'est que vous obtenez une nullvérification exactement comme ça (par exemple if Nothing then Nothing) gratuitement car Maybec'est une monade. C'est encodé dans la définition de bind ( >>=) pour Maybe.
Tikhon Jelvis
2
En outre, concernant (1): vous pourriez facilement écrire une monade pouvant contenir des informations d'erreur (par exemple Either) se comportant exactement comme Maybe. Basculer entre les deux est en fait assez simple car il Maybes’agit en réalité d’un cas particulier Either. (En Haskell, vous pourriez penser Maybeà Either ().)
Tikhon Jelvis
6

"Peut-être" ne remplace pas les exceptions. Les exceptions sont censées être utilisées dans des cas exceptionnels (par exemple, l'ouverture d'une connexion à une base de données et le serveur de base de données n'existe pas, même si elle devrait l'être). "Peut-être" sert à modéliser une situation dans laquelle vous pouvez ou non avoir une valeur valide; disons que vous obtenez une valeur d'un dictionnaire pour une clé: elle peut être là ou peut ne pas être - il n'y a rien d'exceptionnel à propos de ces résultats.

Nemanja Trifunovic
la source
2
En fait, j'ai un peu de code impliquant des dictionnaires où une clé manquante signifie que les choses ont mal tourné à un moment donné, probablement un bogue dans mon code ou dans le code qui a converti l'entrée en ce qui est utilisé à ce moment-là.
3
@ delnan: Je ne dis pas qu'une valeur manquante ne peut jamais être le signe d'un état exceptionnel - elle ne doit tout simplement pas l'être. Peut-être modélise-t-il réellement une situation dans laquelle une variable peut avoir ou non une valeur valide - pensez aux types nullables en C #.
Nemanja Trifunovic
6

J'appuie la réponse de Tikhon, mais je pense qu'il y a un point pratique très important qui manque à tout le monde:

  1. Les mécanismes de traitement des exceptions, du moins dans les langues courantes, sont intimement liés à des threads individuels.
  2. Le Eithermécanisme n'est pas du tout associé aux threads.

Nous constatons donc aujourd'hui que de nombreuses solutions de programmation asynchrone adoptent une variante du Eitherstyle de gestion des erreurs. Considérez les promesses Javascript , comme détaillé dans l’un de ces liens:

Le concept de promesses vous permet d'écrire du code asynchrone comme ceci (tiré du dernier lien):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Fondamentalement, une promesse est un objet qui:

  1. Représente le résultat d'un calcul asynchrone, qui peut être terminé ou non.
  2. Vous permet de chaîner d'autres opérations à effectuer sur son résultat, qui seront déclenchées lorsque ce résultat est disponible, et dont les résultats sont disponibles sous forme de promesses;
  3. Vous permet de connecter un gestionnaire d'échec qui sera appelé si le calcul de la promesse échoue. S'il n'y a pas de gestionnaire, l'erreur est propagée aux gestionnaires ultérieurs de la chaîne.

Fondamentalement, étant donné que la prise en charge des exceptions natives dans le langage ne fonctionne pas lorsque votre calcul s'effectue sur plusieurs threads, une implémentation de promesses doit fournir un mécanisme de traitement des erreurs, qui s'avère être des monades similaires à Maybe/ Eithertypes de Haskell .

sacundim
la source
Il n'a rien à faire avec des threads. JavaScript dans le navigateur s'exécute toujours dans un seul thread et non pas dans plusieurs threads. Mais vous ne pouvez toujours pas utiliser exception car vous ne savez pas quand votre fonction sera appelée dans le futur. Asynchrone ne signifie pas automatiquement une implication de threads. C'est aussi la raison pour laquelle vous ne pouvez pas travailler avec des exceptions. Vous ne pouvez extraire une exception que si vous appelez une fonction et qu'elle est immédiatement exécutée. Mais le but d'Asynchrone est qu'il fonctionne dans le futur, souvent quand quelque chose d'autre se termine, et pas immédiatement. C'est pourquoi vous ne pouvez pas utiliser d'exceptions ici.
David Raab
1

Le système de type Haskell obligera l'utilisateur à reconnaître la possibilité d'un Nothing, alors que les langages de programmation n'exigent souvent pas qu'une exception soit interceptée. Cela signifie que nous saurons, au moment de la compilation, que l'utilisateur a vérifié l'erreur.

Chrisaycock
la source
1
Il existe des exceptions vérifiées en Java. Et les gens les traitent souvent même mal.
Vladimir
1
@ DairT'arg ButJava requiert des contrôles pour les erreurs d'entrée-sortie (à titre d'exemple), mais pas pour les NPE. En Haskell, c'est l'inverse: l'équivalent nul (Maybe) est coché, mais les erreurs d'E / S jettent simplement des exceptions (non cochées). Je ne sais pas quelle différence cela fait, mais je n'entends pas les Haskellers se plaindre de Maybe et je pense que beaucoup de gens souhaiteraient que les NPE soient contrôlés.
1
@delnan Les gens veulent que NPE soit contrôlé? Ils doivent être fous. Et je veux dire ça. Faire cocher NPE n’a de sens que si vous avez un moyen de l’ éviter (en ayant des références non nullables que Java n’a pas).
Konrad Rudolph
5
@ KonradRudolph Oui, les gens ne veulent probablement pas que cela soit vérifié, dans la mesure où ils devront ajouter un throws NPEà chaque signature et catch(...) {throw ...} à chaque corps de méthode. Mais je pense qu’il existe un marché pour les vérifiés dans le même sens que pour Maybe: la nullabilité est facultative et suivie dans le système de types.
1
@delnan Ah, alors je suis d'accord.
Konrad Rudolph
0

Peut-être que la monade est fondamentalement la même chose que la plupart des langages traditionnels utilisent la vérification "erreur de moyen" (sauf que cela nécessite la vérification de la nullité), et a largement les mêmes avantages et inconvénients.

Telastyn
la source
8
Eh bien, il n’a pas les mêmes inconvénients car il peut être vérifié statiquement si il est utilisé correctement. Il n’existe pas d’exception équivalente à une exception de pointeur nul lorsqu’on utilise peut-être des monades (encore une fois, en supposant qu’elles soient utilisées correctement).
Konrad Rudolph
Effectivement. Comment pensez-vous que cela devrait être clarifié?
Telastyn
@ Konrad C'est parce que pour pouvoir utiliser la valeur (éventuellement) dans la catégorie Maybe, vous devez rechercher la valeur Aucune (bien qu'il existe bien entendu la possibilité d'automatiser une grande partie de cette vérification). D'autres langues conservent ce genre de manuel (principalement pour des raisons philosophiques, je crois).
Donal Fellows
2
@Donal Oui, mais notez que la plupart des langages qui implémentent des monades fournissent le sucre syntaxique approprié, de sorte que cette vérification peut souvent être complètement dissimulée. Par exemple, vous pouvez simplement ajouter deux Maybenombres en écrivant a + b sans avoir besoin de vérifier None, et le résultat est encore une fois une valeur facultative.
Konrad Rudolph
2
La comparaison avec les types nullables est valable pour le Maybe type , mais son utilisation en Maybetant que monade ajoute un sucre de syntaxe qui permet d’exprimer la logique null-ish de façon beaucoup plus élégante.
tdammers
0

La gestion des exceptions peut être un réel problème pour la factorisation et les tests. Je sais que python fournit une belle syntaxe "avec" qui vous permet de capturer les exceptions sans le bloc rigide "try ... catch". Mais en Java, par exemple, essayez les blocs catch sont gros, passe-partout, soit verbeux, soit extrêmement verbeux, et difficiles à dissocier. En plus de cela, Java ajoute tout le bruit autour des exceptions vérifiées et non vérifiées.

Si, au contraire, votre monade intercepte des exceptions et les considère comme une propriété de l'espace monadique (au lieu d'une anomalie de traitement), vous êtes libre de mélanger et de faire correspondre les fonctions que vous liez dans cet espace, indépendamment de ce qu'elles lancent ou capturent.

Si, mieux encore, votre monade évite les situations pouvant donner lieu à des exceptions (comme l'envoi d'un chèque nul dans Maybe), c'est encore mieux. Si ... est beaucoup, beaucoup plus facile à factoriser et à tester que d'essayer ... attraper.

D'après ce que j'ai vu, Go adopte une approche similaire en spécifiant que chaque fonction est renvoyée (réponse, erreur). C’est un peu la même chose que de "lever" la fonction dans un espace monade où le type de réponse principal est décoré avec une indication d’erreur, et élimine efficacement les exceptions.

Rob
la source