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.
Try
. La partie concernant Either
vs Exception
indique simplement que Either
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'utiliser Either
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, mais Either
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'un Either
. Scala Either
é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érente Either
mais plutôt le résultat de son biais.