55 votes

Utiliser soit pour traiter les échecs dans le code Scala

Option monade est une grande manière expressive de traiter avec quelque chose-ou-rien des choses en Scala. Mais que faire si on doit se connecter à un message quand "rien" se produit? Selon la Scala de documentation de l'API,

L'un ou l'autre type est souvent utilisé comme un alternative à la scala.Option où de Gauche représente l'échec (par convention) et Droit est semblable à Certains.

Cependant, je n'avais aucune chance de trouver les meilleures pratiques à l'aide de l'une ou de bons exemples du monde réel impliquant Soit pour des échecs de traitement. Enfin j'ai trouvé 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 que ceci est un extrait de un projet réel, de sorte qu'il ne compile pas sur son propre)

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

59voto

Walter Chang Points 7041

L'un est utilisé pour le retour de l'un des deux résultats significatifs, à la différence de l'Option qui est utilisée pour renvoyer un seul résultat significatif, ou rien.

Un exemple facile à comprendre est donnée ci-dessous (diffusée sur la Scala liste de diffusion a tout à l'arrière):

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

Comme le nom de la fonction implique, si l'exécution de "bloc" est un succès, il sera de retour "à Droite(<résultat>)". Sinon, si un Throwable est levée, il sera de retour "à Gauche(<throwable>)". Utiliser le pattern matching 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

Espérons que cela aide.

15voto

fanf42 Points 870

Scalaz bibliothèque a quelque chose de semblable Soit nommé à la Validation. Il est plus idiomatique que ce Soit pour une utilisation comme "obtenir un résultat valide ou un échec".

La Validation permet également d'accumuler les erreurs.

Edit: "comme" l'un des deux est complettly faux, parce que la Validation est un foncteur applicatif, et scalaz Soit, nommé \/ (prononcé "disjonction" ou "soit"), est une monade. Le fait que la Validation puisse accumalate erreurs est en raison de cette nature. D'autre part, a un "stop précoce" de la nature, de s'arrêter à la première\/ (lire "de gauche", ou "erreur") qu'il rencontre. Il y a une parfaite explication 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, copier/coller du 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"))

7voto

Daniel C. Sobral Points 159554

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

  1. Il ne suffit pas de connaître les données n'est pas disponible.
  2. Vous avez besoin de retourner un des deux types distincts.

En tournant une exception dans un Gauche est, en effet, une commune de cas d'utilisation. Plus de try/catch, il a l'avantage de garder l'ensemble du code, qui a de sens que si l'exception est un résultat attendu. La façon la plus courante de la manipulation est le filtrage:

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

Une autre façon intéressante de manutention Either , c'est quand il apparaît dans une collection. Lorsque vous faites une carte sur une collection, de lancer une exception peut ne pas être viable, et vous pouvez retourner des informations autres que "pas possible". À l'aide de l'un ou l'autre vous permet de le faire sans pour autant surcharger l'algorithme:

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

Ici nous avons une liste de tous les auteurs dans la bibliothèque, en plus d'une liste de livres sans auteur. De sorte que nous pouvons plus 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, de base, Soit l'utilisation de la va comme ça. Il n'est pas particulièrement utile de classe, mais si c'était vous auriez vu avant. D'autre part, il n'est pas inutile non plus.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X