Je me demande quels sont les avantages de Maybe
monad par rapport aux exceptions? Il semble que Maybe
soit juste un moyen explicite (et plutôt gourmand en espace) de try..catch
syntaxe.
mise à jour Veuillez noter que je suis intentionnellement pas mentionnant Haskell.
L'utilisation de Maybe
(ou de son cousin Either
qui fonctionne essentiellement de la même manière mais vous permet de renvoyer une valeur arbitraire à la place de Nothing
) a un objectif légèrement différent de celui des exceptions. En Java, c'est comme avoir une exception vérifiée plutôt qu'une exception d'exécution. Cela représente quelque chose attend que vous devez gérer, plutôt qu'une erreur que vous n'avez pas attendre.
Ainsi, une fonction comme indexOf
retournerait une valeur Maybe
car vous vous attendez à la possibilité que l'élément ne soit pas dans la liste. C'est un peu comme retourner null
à partir d'une fonction, sauf d'une manière sûre pour le type qui vous oblige à traiter le cas null
. Either
fonctionne de la même manière sauf que vous pouvez renvoyer des informations associées au cas d'erreur, donc c'est en fait plus similaire à une exception que Maybe
.
Quels sont donc les avantages de l'approche Maybe
/Either
? D'une part, c'est un citoyen de première classe de la langue. Comparons une fonction utilisant Either
à une fonction lançant une exception. Pour le cas d'exception, votre seul véritable recours est un try...catch
déclaration. Pour la fonction Either
, vous pouvez utiliser les combinateurs existants pour rendre le contrôle de flux plus clair. Voici quelques exemples:
Tout d'abord, supposons que vous souhaitiez essayer plusieurs fonctions qui pourraient générer des erreurs d'affilée jusqu'à ce que vous en obteniez une qui ne fonctionne pas. Si vous n'en obtenez pas sans erreurs, vous souhaitez renvoyer un message d'erreur spécial. C'est en fait un modèle très utile, mais ce serait une horrible douleur en utilisant try...catch
. Heureusement, puisque Either
n'est qu'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 facultative. Supposons que vous ayez plusieurs fonctions à exécuter, dont une qui tente d'optimiser une requête. Si cela échoue, vous voulez que tout le reste fonctionne de toute façon. Vous pouvez é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 de try..catch
, et, plus important encore, plus sémantique. Utiliser une fonction comme <|>
ou optional
rend vos intentions beaucoup plus claires que l'utilisation de try...catch
pour toujours gérer les exceptions.
Notez également que vous pas devez joncher votre code avec des lignes comme if a == Nothing then Nothing else ...
! Le but de traiter Maybe
et Either
comme une monade est d'éviter cela. Vous pouvez encoder la sémantique de propagation dans la fonction de liaison afin d'obtenir gratuitement les contrôles null/error. La seule fois où vous devez vérifier explicitement, c'est si vous voulez retourner autre chose que Nothing
étant donné un Nothing
, et même alors c'est facile: il y a un tas de fonctions de bibliothèque standard pour faire ce code plus gentil.
Enfin, un autre avantage est qu'un type Maybe
/Either
est 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 qu'une bibliothèque. Comme ce ne sont que des valeurs normales, cela simplifie le système de types - en Java, vous devez faire la différence entre les types (par exemple, le type de retour) et les effets (par exemple, les instructions throws
) où vous n'utiliseriez pas Maybe
. Ils se comportent également comme tout autre type défini par l'utilisateur - il n'est pas nécessaire d'avoir un code spécial de gestion des erreurs cuit dans la langue.
Une autre victoire est que Maybe
/Either
sont des foncteurs et des monades, ce qui signifie qu'ils peuvent tirer parti des fonctions de flux de contrôle de monade existantes (dont il existe un bon nombre) et, en général, jouer bien avec d'autres monades.
Cela dit, il y a quelques mises en garde. D'une part, ni Maybe
ni Either
ne remplacent non coché les exceptions. Vous voudrez une autre façon de gérer des choses comme la division par 0 simplement parce que ce serait pénible que chaque division retourne une valeur Maybe
.
Un autre problème consiste à renvoyer plusieurs types d'erreurs (cela ne s'applique qu'à Either
). Avec les exceptions, vous pouvez lancer différents types d'exceptions dans la même fonction. avec Either
, vous obtenez un seul type. Cela peut être surmonté avec sous-typage ou un ADT contenant tous les différents types d'erreurs en tant que constructeurs (cette deuxième approche est ce qui est généralement utilisé dans Haskell).
Néanmoins, je préfère avant tout l'approche Maybe
/Either
car je la trouve plus simple et plus flexible.
OpenFile()
peut lancer FileNotFound
ou NoPermission
ou TooManyDescriptors
etc. A Aucun ne transporte pas ces informations.if None return None
.Plus important encore, une exception et une monade peut-être ont des objectifs différents - une exception est utilisée pour signifier un problème, tandis qu'un peut-être ne l'est pas.
"Infirmière, si il y a un patient dans la salle 5, pouvez-vous lui demander d'attendre?"
(remarquez le "si" - cela signifie que le médecin attend une monade peut-être)
J'appuie la réponse de Tikhon, mais je pense qu'il y a un point pratique très important que tout le monde manque:
Either
n'est pas du tout couplé aux threads.Donc, quelque chose que nous voyons de nos jours dans la vie réelle, c'est que de nombreuses solutions de programmation asynchrone adoptent une variante du style Either
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 celui-ci (extrait 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:
Fondamentalement, comme la prise en charge des exceptions natives du langage ne fonctionne pas lorsque votre calcul se déroule sur plusieurs threads, une implémentation de promesses doit fournir un mécanisme de gestion des erreurs, et celles-ci se révèlent être des monades similaires à Maybe
/de Haskell Either
types.
"Peut-être" ne remplace pas les exceptions. Les exceptions sont destinées à être utilisées dans des cas exceptionnels (par exemple: l'ouverture d'une connexion db et le serveur db n'est pas là bien qu'il devrait l'être). "Peut-être" sert à modéliser une situation où vous pouvez ou non avoir une valeur valide; dites que vous obtenez une valeur d'un dictionnaire pour une clé: elle peut être là ou peut-être pas - il n'y a rien "d'exceptionnel" dans aucun de ces résultats.
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 recherché une erreur.
La gestion des exceptions peut être un vrai problème pour l'affacturage et les tests. Je sais python fournit une syntaxe Nice "avec" qui vous permet d'intercepter les exceptions sans le bloc rigide "try ... catch". Mais en Java, par exemple, les blocs try catch sont gros, passe-partout , verbeux ou extrêmement verbeux, et difficile à décomposer. En plus de cela, Java ajoute tout le bruit autour des exceptions vérifiées et non contrôlées.
Si, au lieu de cela, votre monade intercepte des exceptions et les traite comme une propriété de l'espace monadique (au lieu d'une anomalie de traitement), alors 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 attrapent.
Si, mieux encore, votre monade empêche les conditions où des exceptions pourraient se produire (comme pousser une vérification nulle dans Peut-être), alors encore mieux. si ... alors il est beaucoup plus facile de factoriser et de tester que d'essayer ... d'attraper.
D'après ce que j'ai vu, Go adopte une approche similaire en spécifiant que chaque fonction renvoie (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é d'une indication d'erreur, et de contourner efficacement les exceptions de lancement et de capture.
La monade peut-être est fondamentalement la même que la plupart des langages courants utilisant la vérification "null signifie erreur" (sauf qu'elle requiert la vérification de la valeur null), et présente en grande partie les mêmes avantages et inconvénients.