Il n'y a pas si longtemps, j'ai commencé à utiliser Scala au lieu de Java. Une partie du processus de «conversion» entre les langues pour moi a été d'apprendre à utiliser Either
s au lieu de (coché) Exception
s. J'ai codé de cette façon pendant un certain temps, mais récemment, j'ai commencé à me demander si c'était vraiment une meilleure façon de procéder.
Un avantage majeur Either
a plus de Exception
meilleures performances; un Exception
besoin de construire une trace de pile et est lancé. Pour autant que je comprends, cependant, lancer la Exception
n'est pas la partie exigeante, mais construire la pile-trace l'est.
Mais alors, on peut toujours construire / hériter de Exception
s scala.util.control.NoStackTrace
, et plus encore, je vois beaucoup de cas où le côté gauche d'un Either
est en fait un Exception
(renoncer à l'augmentation des performances).
Un autre avantage Either
a la sécurité du compilateur; le compilateur Scala ne se plaindra pas des Exception
s non gérés (contrairement au compilateur Java). Mais si je ne me trompe pas, cette décision est motivée par le même raisonnement qui est discuté dans ce sujet, alors ...
En termes de syntaxe, je pense que Exception
-style est beaucoup plus clair. Examinez les blocs de code suivants (les deux réalisant les mêmes fonctionnalités):
Either
style:
def compute(): Either[String, Int] = {
val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")
val bEithers: Iterable[Either[String, Int]] = someSeq.map {
item => if (someCondition(item)) Right(item.toInt) else Left("bad")
}
for {
a <- aEither.right
bs <- reduce(bEithers).right
ignore <- validate(bs).right
} yield compute(a, bs)
}
def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code
def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()
def compute(a: String, bs: Iterable[Int]): Int = ???
Exception
style:
@throws(classOf[ComputationException])
def compute(): Int = {
val a = if (someCondition) "good" else throw new ComputationException("bad")
val bs = someSeq.map {
item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
}
if (bs.sum > 22) throw new ComputationException("bad")
compute(a, bs)
}
def compute(a: String, bs: Iterable[Int]): Int = ???
Ce dernier me semble beaucoup plus propre, et le code gérant l'échec (soit la correspondance de modèle sur Either
ou try-catch
) est assez clair dans les deux cas.
Donc ma question est - pourquoi utiliser Either
plus (vérifié)Exception
?
Mise à jour
Après avoir lu les réponses, je me suis rendu compte que je n'avais peut-être pas présenté le cœur de mon dilemme. Je ne m'inquiète pas de l'absence de la try-catch
; on peut soit "attraper" un Exception
avec Try
, soit utiliser catch
pour envelopper l'exception avecLeft
.
Mon principal problème avec Either
/Try
survient lorsque j'écris du code qui peut échouer à de nombreux points en cours de route; dans ces scénarios, lorsque je rencontre un échec, je dois propager cet échec dans tout mon code, rendant ainsi le code beaucoup plus lourd (comme illustré dans les exemples susmentionnés).
Il existe en fait une autre façon de casser le code sans Exception
s en utilisant return
(qui est en fait un autre "tabou" dans Scala). Le code serait encore plus clair que l' Either
approche, et tout en étant un peu moins propre que le Exception
style, il n'y aurait pas à craindre que l' Exception
art.
def compute(): Either[String, Int] = {
val a = if (someCondition) "good" else return Left("bad")
val bs: Iterable[Int] = someSeq.map {
item => if (someCondition(item)) item.toInt else return Left("bad")
}
if (bs.sum > 22) return Left("bad")
val c = computeC(bs).rightOrReturn(return _)
Right(computeAll(a, bs, c))
}
def computeC(bs: Iterable[Int]): Either[String, Int] = ???
def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???
implicit class ConvertEither[L, R](either: Either[L, R]) {
def rightOrReturn(f: (Left[L, R]) => R): R = either match {
case Right(r) => r
case Left(l) => f(Left(l))
}
}
Fondamentalement, les return Left
remplacements throw new Exception
et la méthode implicite sur l'un ou l'autre rightOrReturn
sont un supplément pour la propagation automatique des exceptions dans la pile.
la source
Try
. La partie concernantEither
vsException
indique simplement queEither
s devrait être utilisé lorsque l'autre cas de la méthode est "non exceptionnel". Tout d'abord, c'est une définition très, très vague à mon humble avis. Deuxièmement, cela vaut-il vraiment la peine de syntaxe? Je veux dire, cela ne me dérangerait vraiment pas d'utiliserEither
s s'il n'y avait pas la surcharge de syntaxe qu'ils présentent.Either
ressemble à une monade pour moi. Utilisez-le lorsque vous avez besoin des avantages de composition fonctionnelle qu'offrent les monades. Ou peut - être pas .Either
en soi n'est pas une monade. La projection vers le côté gauche ou le côté droit est une monade, maisEither
ne l'est pas en soi. Vous pouvez en faire une monade, en la "biaisant" du côté gauche ou du côté droit, cependant. Cependant, alors vous donnez une certaine sémantique des deux côtés d'unEither
. ScalaEither
était à l'origine impartiale, mais était biaisée assez récemment, de sorte qu'aujourd'hui, c'est en fait une monade, mais la "monadness" n'est pas une propriété inhérenteEither
mais plutôt le résultat de son biais.Réponses:
Si vous utilisez uniquement un bloc
Either
exactement comme un impératiftry-catch
, bien sûr, cela ressemblera à une syntaxe différente pour faire exactement la même chose.Eithers
sont une valeur . Ils n'ont pas les mêmes limitations. Vous pouvez:Eithers
n'a pas besoin d'être juste à côté de la partie "try". Il peut même être partagé dans une fonction commune. Cela facilite beaucoup la séparation des préoccupations.Eithers
dans des structures de données. Vous pouvez les parcourir et consolider les messages d'erreur, trouver le premier qui n'a pas échoué, les évaluer paresseusement ou similaire.Eithers
? Vous pouvez écrire des fonctions d'assistance pour les rendre plus faciles à utiliser pour vos cas d'utilisation particuliers. Contrairement à try-catch, vous n'êtes pas bloqué en permanence avec uniquement l'interface par défaut proposée par les concepteurs de langage.Remarque pour la plupart des cas d'utilisation, a
Try
a une interface plus simple, présente la plupart des avantages ci-dessus et répond généralement à vos besoins. UnOption
est encore plus simple si vous ne vous souciez que du succès ou de l'échec et n'avez besoin d'aucune information d'état comme des messages d'erreur sur le cas d'échec.D'après mon expérience,
Eithers
ne sont pas vraiment préférés àTrys
moins que leLeft
soit autre chose qu'unException
, peut-être un message d'erreur de validation, et que vous vouliez agréger lesLeft
valeurs. Par exemple, vous traitez une entrée et vous souhaitez collecter tous les messages d'erreur, pas seulement une erreur sur le premier. Ces situations ne se présentent pas très souvent dans la pratique, du moins pour moi.Regardez au-delà du modèle try-catch et vous trouverez
Eithers
beaucoup plus agréable à travailler.Mise à jour: cette question retient toujours l'attention, et je n'avais pas vu la mise à jour de l'OP clarifier la question de syntaxe, alors j'ai pensé en ajouter plus.
À titre d'exemple pour sortir de l'état d'esprit d'exception, voici comment j'écrirais généralement votre exemple de code:
Ici, vous pouvez voir que la sémantique de
filterOrElse
capture parfaitement ce que nous faisons principalement dans cette fonction: vérifier les conditions et associer des messages d'erreur aux échecs donnés. Comme il s'agit d'un cas d'utilisation très courant,filterOrElse
trouve dans la bibliothèque standard, mais si ce n'était pas le cas, vous pourriez le créer.C'est le point où quelqu'un arrive avec un contre-exemple qui ne peut pas être résolu précisément de la même manière que le mien, mais le point que j'essaie de faire est qu'il n'y a pas qu'une seule façon d'utiliser
Eithers
, contrairement aux exceptions. Il existe des moyens de simplifier le code pour la plupart des situations de gestion d'erreurs, si vous explorez toutes les fonctions disponiblesEithers
et que vous êtes ouvert à l'expérimentation.la source
try
etcatch
est un argument juste, mais cela ne pourrait-il pas être facilement réalisé avecTry
? 2. Le stockage desEither
s dans les structures de données pourrait se faire parallèlement authrows
mécanisme; n'est-ce pas simplement une couche supplémentaire en plus de gérer les échecs? 3. Vous pouvez certainement étendre l'Exception
art. Bien sûr, vous ne pouvez pas changer latry-catch
structure, mais la gestion des échecs est si courante que je veux vraiment que le langage me donne un passe-partout pour cela (que je ne suis pas obligé d'utiliser). C'est comme dire que la compréhension est mauvaise parce que c'est un passe-partout pour les opérations monadiques.Either
s peuvent être étendus, là où clairement ils ne le peuvent pas (étant unsealed trait
avec seulement deux implémentations, les deuxfinal
). Pourriez-vous préciser ceci? Je suppose que vous ne vouliez pas dire le sens littéral de l'extension.Les deux sont en fait très différents.
Either
La sémantique est beaucoup plus générale que de représenter simplement des calculs potentiellement défaillants.Either
vous permet de renvoyer l'un des deux types différents. C'est ça.Maintenant, l'un de ces deux types peut ou non représenter une erreur et l'autre peut ou non représenter un succès, mais ce n'est qu'un cas d'utilisation possible parmi tant d'autres.
Either
est beaucoup, beaucoup plus général que cela. Vous pourriez tout aussi bien avoir une méthode qui lit les entrées de l'utilisateur qui est autorisé à saisir un nom ou à renvoyer une dateEither[String, Date]
.Ou, pensez à lambda littéraux dans C♯: ils évaluent soit à un
Func
ouExpression
, à savoir qu'ils évaluent soit à une fonction anonyme ou ils évaluent à un AST. En C♯, cela se produit "par magie", mais si vous vouliez lui donner un type correct, ce serait le casEither<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>
. Cependant, aucune des deux options n'est une erreur, et aucune des deux n'est "normale" ou "exceptionnelle". Ce ne sont que deux types différents qui peuvent être renvoyés par la même fonction (ou dans ce cas, attribués au même littéral). [Remarque: en fait,Func
doit être divisé en deux autres cas, car C♯ n'a pas deUnit
type, et donc quelque chose qui ne renvoie rien ne peut pas être représenté de la même manière que quelque chose qui renvoie quelque chose,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>
Maintenant,
Either
peut être utilisé pour représenter un type de réussite et un type d'échec. Et si vous êtes d' accord sur une commande cohérente des deux types, vous pouvez même biaisEither
préférer l' un des deux types, permettant ainsi de propager des échecs par l' enchaînement monadique. Mais dans ce cas, vous donnez une certaine sémantique àEither
ce qu'elle ne possède pas nécessairement d'elle-même.Un
Either
qui a l'un de ses deux types fixé pour être un type d'erreur et est biaisé d'un côté pour propager les erreurs, est généralement appelé uneError
monade et serait un meilleur choix pour représenter un calcul potentiellement défaillant. Dans Scala, ce type de type est appeléTry
.Il est beaucoup plus logique de comparer l'utilisation des exceptions avec
Try
qu'avecEither
.Donc, c'est la raison # 1 pour laquelle on
Either
pourrait utiliser les exceptions vérifiées:Either
est beaucoup plus général que les exceptions. Il peut être utilisé dans les cas où les exceptions ne s'appliquent même pas.Cependant, vous posiez probablement des questions sur le cas restreint où vous utilisez
Either
(ou plus probablementTry
) en remplacement d'exceptions. Dans ce cas, il y a encore quelques avantages, ou plutôt des approches différentes possibles.Le principal avantage est que vous pouvez différer la gestion de l'erreur. Vous stockez simplement la valeur de retour, sans même regarder si c'est un succès ou une erreur. (Manipulez-le plus tard.) Ou passez-le ailleurs. (Laissez quelqu'un d'autre s'en préoccuper.) Rassemblez-les dans une liste. (Gérez-les tous en même temps.) Vous pouvez utiliser le chaînage monadique pour les propager le long d'un pipeline de traitement.
La sémantique des exceptions est câblée dans la langue. Dans Scala, les exceptions déroulent la pile, par exemple. Si vous les laissez remonter à un gestionnaire d'exceptions, le gestionnaire ne peut pas revenir là où cela s'est produit et le corriger. OTOH, si vous l'attrapez plus bas dans la pile avant de le dérouler et de détruire le contexte nécessaire pour le corriger, alors vous pourriez être dans un composant trop bas pour prendre une décision éclairée sur la façon de procéder.
Ceci est différent en Smalltalk, par exemple, où les exceptions ne sont pas détendre la pile et sont resumable, et vous pouvez attraper le haut d'exception dans la pile (peut - être aussi haut que le débogueur), réparer l'erreur waaaaayyyyy vers le bas dans la empiler et reprendre l'exécution à partir de là. Ou les conditions CommonLisp où élever, attraper et manipuler sont trois choses différentes au lieu de deux (élever et attraper-manipuler) comme avec des exceptions.
L'utilisation d'un type de retour d'erreur réel au lieu d'une exception vous permet de faire des choses similaires, et plus encore, sans avoir à modifier la langue.
Et puis il y a l'éléphant évident dans la pièce: les exceptions sont un effet secondaire. Les effets secondaires sont mauvais. Remarque: je ne dis pas que cela est nécessairement vrai. Mais c'est un point de vue que certaines personnes ont, et dans ce cas, l'avantage d'un type d'erreur sur les exceptions est évident. Donc, si vous voulez faire de la programmation fonctionnelle, vous pouvez utiliser des types d'erreur pour la seule raison qu'ils sont l'approche fonctionnelle. (Notez que Haskell a des exceptions. Notez également que personne n'aime les utiliser.)
la source
Exception
art.Either
semble être un excellent outil, mais oui, je pose spécifiquement des questions sur la gestion des échecs, et je préfère utiliser un outil beaucoup plus spécifique pour ma tâche qu'un outil tout-puissant. Le report de la gestion des échecs est un argument équitable, mais on peut simplement utiliserTry
une méthode qui lance unException
s'il souhaite ne pas le gérer pour le moment. Ne pas dérouler pile trace affectEither
/Try
ainsi? Vous pouvez répéter les mêmes erreurs lors de leur mauvaise propagation; savoir quand gérer un échec n'est pas anodin dans les deux cas.La bonne chose à propos des exceptions est que le code d'origine a déjà classé la circonstance exceptionnelle pour vous. La différence entre un
IllegalStateException
et unBadParameterException
est beaucoup plus facile à différencier dans des langages typés plus forts. Si vous essayez d'analyser "bon" et "mauvais", vous ne profitez pas de l'outil extrêmement puissant à votre disposition, à savoir le compilateur Scala. Vos propres extensionsThrowable
peuvent inclure autant d'informations que vous le souhaitez, en plus de la chaîne de message textuel.Il s'agit de la véritable utilité de
Try
l'environnement Scala, avec leThrowable
rôle d'enveloppe des éléments laissés pour vous faciliter la vie.la source
Either
a des problèmes de style car ce n'est pas une vraie monade (?); l'arbitraire de laleft
valeur s'éloigne de la valeur ajoutée plus élevée lorsque vous enfermez correctement les informations sur les conditions exceptionnelles dans une structure appropriée. Donc, il n'est pas correct de comparer l'utilisation de,Either
dans un cas, le retour de chaînes dans le côté gauche ettry/catch
dans l'autre cas, en utilisant correctement typéThrowables
. En raison de la valeur deThrowables
fournir des informations, et de la manière concise quiTry
gèreThrowables
,Try
est un meilleur pari que ...Either
outry/catch
try-catch
. Comme dit dans la question, ce dernier me semble beaucoup plus propre, et le code gérant l'échec (soit le filtrage des motifs sur Either soit le try-catch) est assez clair dans les deux cas.