web-dev-qa-db-fra.com

Utiliser soit pour traiter les échecs dans le code Scala

Option monad est un excellent moyen expressif de gérer quelque chose ou rien à Scala. Mais que se passe-t-il si on doit enregistrer un message quand "rien" ne se produit? Selon la documentation de l'API Scala,

L'un ou l'autre type est souvent utilisé comme alternative À scala.Option où Left Représente un échec (par convention) et Right s'apparente à Some.

Cependant, je n’ai pas eu la chance de trouver les meilleures pratiques en utilisant soit d’excellents exemples concrets impliquant Euler pour les échecs de traitement. Enfin, j'ai développé le code suivant pour mon propre projet:

    def logs: Array[String] = {
        def props: Option[Map[String, Any]] = configAdmin.map{ ca =>
            val config = ca.getConfiguration(PID, null)
            config.properties getOrElse immutable.Map.empty
        }
        def checkType(any: Any): Option[Array[String]] = any match {
            case a: Array[String] => Some(a)
            case _ => None
        }
        def lookup: Either[(Symbol, String), Array[String]] =
            for {val properties <- props.toRight('warning -> "ConfigurationAdmin service not bound").right
                 val logsParam <- properties.get("logs").toRight('debug -> "'logs' not defined in the configuration").right
                 val array <- checkType(logsParam).toRight('warning -> "unknown type of 'logs' confguration parameter").right}
            yield array

        lookup.fold(failure => { failure match {
            case ('warning, msg) => log(LogService.WARNING, msg)
            case ('debug, msg) =>   log(LogService.DEBUG, msg)
            case _ =>
        }; new Array[String](0) }, success => success)
    }

(Veuillez noter qu'il s'agit d'un extrait d'un projet réel, afin qu'il ne soit pas compilé seul)

Je serais reconnaissant de savoir comment vous utilisez Either dans votre code et/ou de meilleures idées pour la refactorisation du code ci-dessus.

49
Alexander Azarov

L'une ou l'autre est utilisée pour renvoyer l'un des deux résultats significatifs possibles, contrairement à Option qui est utilisée pour renvoyer un seul résultat significatif ou rien.

Un exemple facile à comprendre est donné ci-dessous (distribué il y a quelque temps sur la liste de diffusion Scala):

def throwableToLeft[T](block: => T): Either[Java.lang.Throwable, T] =
  try {
    Right(block)
  } catch {
    case ex => Left(ex)
  }

Comme le nom de la fonction l’implique, si l’exécution de "block" aboutit, le résultat sera renvoyé "Right (<résultat>)". Sinon, si un Throwable est lancé, il retournera "Left (<throwable>)". Utilisez la correspondance de modèle pour traiter le résultat:

var s = "hello"
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints "HELLO"

s = null
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints NullPointerException stack trace

J'espère que cela pourra aider.

48
Walter Chang

La bibliothèque Scalaz a quelque chose de similaire Soit nommé Validation. Il est plus idiomatique que d'utiliser soit "obtenir un résultat valide ou un échec".

La validation permet également d’accumuler des erreurs.

Edit: "pareil" L'un ou l'autre est complètement faux, car la validation est un foncteur applicatif, et scalaz l'un ou l'autre, nommé \/(prononcé "disjonction" ou "l'un ou l'autre"), est une monade. Le fait que la validation puisse accumuler des erreurs est dû à cette nature. D'autre part,/a un caractère "arrêter tôt", en s'arrêtant au premier - \/(lisez-le "à gauche", ou "erreur") qu'il rencontre. Il y a une explication parfaite ici: http://typelevel.org/blog/2014/02/21/error-handling.html

Voir: http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html

Comme demandé par le commentaire, copiez/collez le lien ci-dessus (quelques lignes supprimées):

// Extracting success or failure values
val s: Validation[String, Int] = 1.success
val f: Validation[String, Int] = "error".fail

// It is recommended to use fold rather than pattern matching:
val result: String = s.fold(e => "got error: " + e, s => "got success: " + s.toString)

s match {
  case Success(a) => "success"
  case Failure(e) => "fail"
}

// Validation is a Monad, and can be used in for comprehensions.
val k1 = for {
  i <- s
  j <- s
} yield i + j
k1.toOption assert_≟ Some(2)

// The first failing sub-computation fails the entire computation.
val k2 = for {
  i <- f
  j <- f
} yield i + j
k2.fail.toOption assert_≟ Some("error")

// Validation is also an Applicative Functor, if the type of the error side of the validation is a Semigroup.
// A number of computations are tried. If the all success, a function can combine them into a Success. If any
// of them fails, the individual errors are accumulated.

// Use the NonEmptyList semigroup to accumulate errors using the Validation Applicative Functor.
val k4 = (fNel <**> fNel){ _ + _ }
k4.fail.toOption assert_≟ some(nel1("error", "error"))
13
fanf42

L'extrait que vous avez posté semble très artificiel. Vous utilisez soit dans une situation où:

  1. Il ne suffit pas de savoir que les données ne sont pas disponibles.
  2. Vous devez renvoyer l'un des deux types distincts.

Transformer une exception en gauche est en effet un cas d'utilisation courant. Au lieu de try/catch, il a l’avantage de garder le code ensemble, ce qui est logique si l’exception est un résultat attendu . La méthode de traitement la plus courante consiste à rechercher des modèles:

result match {
  case Right(res) => ...
  case Left(res) => ...
}

Un autre moyen intéressant de manipuler Either consiste à le faire apparaître dans une collection. Lors de la création d'une carte sur une collection, le lancement d'une exception peut ne pas être viable et vous pouvez souhaiter renvoyer des informations autres que "impossible". Utiliser un des deux permet de le faire sans surcharger l'algorithme:

val list = (
  library 
  \\ "books" 
  map (book => 
    if (book \ "author" isEmpty) 
      Left(book) 
    else 
      Right((book \ "author" toList) map (_ text))
  )
)

Nous avons ici une liste de tous les auteurs de la bibliothèque, plus une liste de livres sans auteur. Nous pouvons donc ensuite le traiter en conséquence:

val authorCount = (
  (Map[String,Int]() /: (list filter (_ isRight) map (_.right.get))) 
   ((map, author) => map + (author -> (map.getOrElse(author, 0) + 1)))
  toList
)
val problemBooks = list flatMap (_.left.toSeq) // thanks to Azarov for this variation

Donc, l’utilisation de base est la même. Ce n'est pas une classe particulièrement utile, mais si c'était le cas, vous l'auriez déjà vu auparavant. D'un autre côté, ce n'est pas inutile non plus.

7
Daniel C. Sobral

Cats a un bon moyen de créer un code Soit à partir d'exception:

val either: Either[NumberFormatException, Int] =
  Either.catchOnly[NumberFormatException]("abc".toInt)
// either: Either[NumberFormatException,Int] = Left(Java.lang.NumberFormatException: For input string: "abc")

in https://typelevel.org/cats/datatypes/either.html#working-with-exception-y-code

0
vlfig