Uso di Either per elaborare i fallimenti nel codice Scala

Option monad è un ottimo modo espressivo per gestire qualcosa o qualcosa in Scala. Ma cosa succede se è necessario registrare un messaggio quando “nulla” si verifica? Secondo la documentazione API Scala,

L’Either type è spesso usato come alternativa a scala.Option dove Left rappresenta il fallimento (per convenzione) e Right è simile a Some.

Tuttavia, non ho avuto fortuna di trovare le migliori pratiche che utilizzano Either o esempi di esempi reali che coinvolgono Either per l’elaborazione dei guasti. Finalmente ho trovato il seguente codice per il mio progetto:

  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  "ConfigurationAdmin service not bound").right val logsParam  "'logs' not defined in the configuration").right val array  "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) } 

(Si noti che questo è uno snippet di un progetto reale, quindi non verrà compilato da solo)

Sarei grato di sapere come stai usando Either nel tuo codice e / o idee migliori sul refactoring del codice sopra.

O è usato per restituire uno dei due possibili risultati significativi, diversamente da Opzione che viene utilizzata per restituire un singolo risultato significativo o nulla.

Di seguito è riportato un esempio di facile comprensione (circolato sulla mailing list di Scala qualche tempo fa):

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

Come suggerisce il nome della funzione, se l’esecuzione di “blocco” ha esito positivo, verrà restituito “Destro ()”. Altrimenti, se un Throwable viene lanciato, restituirà “Left ()”. Usa la corrispondenza del modello per elaborare il risultato:

 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 

Spero possa aiutare.

La libreria Scalaz ha qualcosa di simile O ha chiamato Validation. È più idiomatico di quanto sia per “ottenere un risultato valido o un fallimento”.

La convalida consente anche di accumulare errori.

Modifica: “simile” O è completamente falso, perché Validazione è un funtore applicativo, e scalaz O, chiamato \ / (pronunciato “disgiunzione” o “entrambi”), è una monade. Il fatto che Validazione possa accumulare errori è a causa di questa natura. D’altra parte, / ha una natura “stop early”, fermandosi al primo – \ / (leggilo “left”, o “error”) che incontra. C’è una spiegazione perfetta qui: http://typelevel.org/blog/2014/02/21/error-handling.html

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

Come richiesto dal commento, copia / incolla del link precedente (alcune linee rimosse):

 // 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")) 

Il frammento che hai postato sembra molto elaborato. Si utilizza o in una situazione in cui:

  1. Non è sufficiente sapere solo che i dati non sono disponibili.
  2. Devi restituire uno dei due tipi distinti.

Trasformare un’eccezione in una sinistra è, in effetti, un caso d’uso comune. Over try / catch, ha il vantaggio di mantenere unito il codice, il che ha senso se l’eccezione è un risultato previsto . Il modo più comune di gestione O è la corrispondenza del modello:

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

Un altro modo interessante di gestire Either è quando appare in una collezione. Quando si esegue una mappa su una raccolta, lanciare un’eccezione potrebbe non essere valida e si potrebbe voler restituire alcune informazioni diverse da “non ansible”. L’uso di un Either ti consente di farlo senza sovraccaricare l’algoritmo:

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

Qui otteniamo un elenco di tutti gli autori nella libreria, oltre a un elenco di libri senza un autore. Quindi possiamo quindi elaborarlo di conseguenza:

 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 

Quindi, l’uso di entrambi è così. Non è una lezione particolarmente utile, ma se lo avessi visto prima. D’altra parte, non è neanche inutile.