web-dev-qa-db-fra.com

Pourquoi utiliser une exception sur (cochée)?

Il n'y a pas trop longtemps, j'ai commencé à utiliser Scala au lieu de Java. Une partie du processus "Conversion" entre les langues pour moi a appris à utiliser Eithers au lieu de (vérifié) Exceptions. J'ai codé de cette façon pour un moment, mais récemment, j'ai commencé à me demander si c'est vraiment une meilleure façon de partir.

Un avantage majeur Either a sur Exception est une meilleure performance; Un Exception doit construire une trace de pile et est lancée. Autant que je sache, cependant, jetant le Exception n'est pas la partie exigeante, mais la construction de la trace de pile est.

Mais alors, on peut toujours construire/hériter Exceptions avec scala.util.control.NoStackTrace, et encore plus encore, je vois beaucoup de cas où le côté gauche d'un Either est en fait un Exception (fortifier la performance boost).

Un autre avantage Either a le compilateur-sécurité; le Scala === Compiler ne se plaingait pas de non manipulé Exceptions (contrairement au compilateur de Java). Mais si je ne me trompe pas, cette décision est motivée par le même raisonnement que est discuté dans ce sujet, alors ...

En termes de syntaxe, je me sens comme Exception- style est bien plus clair. Examinez les blocs de code suivants (atteignant la même fonctionnalité):

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 a l'air beaucoup plus propre pour moi et le code qui manipule la défaillance (soit une correspondance de modèle sur Either ou try-catch) est assez clair dans les deux cas.

Donc, ma question est - pourquoi utiliser Either sur (coché) Exception?

Mise à jour

Après avoir lu les réponses, j'ai réalisé que j'aurais pu avoir échoué au cœur de mon dilemme. Ma préoccupation est non avec le manque de try-catch; On peut soit "attraper" un Exception avec Try, ou utilisez le catch pour envelopper l'exception avec Left.

Mon problème principal avec Either/Try vient lorsque j'écris du code qui pourrait échouer à de nombreux points en cours de route; Dans ces scénarios, lors de la rencontre d'une défaillance, je dois propager cette défaillance tout au long de mon code, rendant ainsi le code de manière plus encombrante (comme indiqué dans les exemples susmentionnés).

Il y a en fait une autre façon de casser le code sans Exceptions en utilisant return (qui est en fait un autre "tabou" dans SCALA). Le code serait toujours plus clair que l'approche Either, et tout en étant un peu moins propre que le style Exception, il n'y aurait aucune crainte de non capturé Exceptions.

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 le return Left remplace throw new Exception, et la méthode implicite sur l'un ou l'autre, rightOrReturn, est un complément pour la propagation automatique des exceptions sur la pile.

17
Eyal Roth

Si vous utilisez seulement un Either exactement comme un impératif try-catch Block, bien sûr, il va ressembler à 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:

  • Arrêtez de votre habitude de garder vos blocs d'essai petit et contigu. La partie "capture" de Eithers n'a pas besoin d'être juste à côté de la partie "Essayer". Il peut même être partagé dans une fonction commune. Cela facilite beaucoup plus facilement les préoccupations correctement.
  • Stockez Eithers dans les structures de données. Vous pouvez les boucler et consolider les messages d'erreur, trouver le premier qui n'a pas échoué, les évalue paresseusement, ou similaire.
  • Étendre et les personnaliser. Vous n'aimez pas que certains chaudières vous êtes obligés d'écrire avec 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é de manière permanente avec seulement l'interface par défaut que les concepteurs de langue sont venus.

Remarque Pour la plupart des cas d'utilisation, un Try a une interface plus simple, possède la plupart des avantages ci-dessus et répond généralement à vos besoins tout aussi. Un Option est encore plus simple si tout ce que vous aimez est le succès ou l'échec et n'avez besoin d'aucune information d'état comme des messages d'erreur sur le cas de défaillance.

Dans mon expérience, Eithers _ ne sont pas vraiment préférés sur Trys si si le Left est autre chose qu'un message Exception, peut-être un message d'erreur de validation, et vous Voulez-vous agréger les valeurs Left. Par exemple, vous traitez certaines entrées et vous souhaitez collecter tous les messages d'erreur, pas seulement une erreur sur la première. 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 avec.

Mise à jour: Cette question attire toujours l'attention et je n'avais pas vu la mise à jour de l'OP clarifier la question de la syntaxe, alors je pensais ajouter plus.

À titre d'exemple de rupture de la mental d'exception, voici comment je voudrais généralement écrire votre code d'exemple:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

Ici, vous pouvez voir que la sémantique de filterOrElse _ Capturez parfaitement ce que nous faisons principalement dans cette fonction: Vérification des conditions et associer des messages d'erreur avec les échecs donnés. Comme il s'agit d'un cas d'utilisation très courant, filterOrElse est dans la bibliothèque standard, mais si ce n'était pas, vous pourriez le créer.

C'est le point où quelqu'un propose un contre-exemple qui ne peut pas être résolu précisément de la même manière que la mienne, mais le point que j'essaie de faire est qu'il n'y a pas qu'un moyen d'utiliser Eithers , contrairement à des exceptions. Il existe des moyens de simplifier le code pour la plupart des situations de traitement des erreurs, si vous explorez tous Les fonctions disponibles à utiliser avec Eithers et sont ouverts à l'expérimentation.

13
Karl Bielefeldt

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 un BadParameterException est beaucoup plus facile de différencier les langages dactylographiés plus forts. Si vous essayez d'analyser "bon" et "mauvais", vous ne profitiez pas de l'outil extrêmement puissant à votre disposition, à savoir le Scala compiler. Vos extensions à Throwable Peut inclure autant d'informations que nécessaire, en plus de la chaîne de message textuelle.

C'est la véritable utilité de Try dans le Scala Environment, avec Throwable agissant en tant que wrapper d'éléments de gauche pour faciliter la vie.

0
BobDalgleish