Non molto tempo fa ho iniziato a utilizzare Scala anziché Java. Parte del processo di "conversione" tra le lingue per me stava imparando a usare Either
s invece di (controllato) Exception
s. Ho scritto questo codice per un po ', ma recentemente ho iniziato a chiedermi se fosse davvero un modo migliore di andare.
Uno dei principali vantaggi Either
ha oltre Exception
è una prestazione migliore; un Exception
deve compilare una traccia dello stack e viene lanciato. Per quanto ho capito, però, il lancio di Exception
non è la parte più impegnativa, ma è la costruzione della traccia dello stack.
Ma poi, si può sempre costruire / ereditare Exception
s con scala.util.control.NoStackTrace
, e ancora di più, vedo un sacco di casi in cui il lato sinistro di Either
è in effetti un Exception
(in attesa del incremento delle prestazioni).
Un altro vantaggio Either
ha è compilatore-sicurezza; il compilatore di Scala non si lamenterà della percentuale non gestita di Exception
s (diversamente dal compilatore di Java). Ma se non sbaglio, questa decisione è ragionata con lo stesso ragionamento che viene discusso in questo argomento, quindi ...
In termini di sintassi, mi sembra che Exception
-style sia molto più chiaro. Esaminare i seguenti blocchi di codice (entrambi con la stessa funzionalità):
Either
stile:
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
stile:
@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 = ???
Quest'ultimo aspetto mi sembra molto più pulito e il codice che gestisce l'errore (sia la corrispondenza del modello su Either
o try-catch
) è abbastanza chiaro in entrambi i casi.
Quindi la mia domanda è: perché usare Either
sopra (controllato) Exception
?
Aggiorna
Dopo aver letto le risposte, mi sono reso conto che avrei potuto non presentare il nocciolo del mio dilemma. La mia preoccupazione è non con la mancanza di try-catch
; puoi "catturare" un Exception
con Try
, oppure usare catch
per avvolgere l'eccezione con Left
.
Il mio problema principale con Either
/ Try
arriva quando scrivo codice che potrebbe fallire in molti punti lungo la strada; in questi scenari, quando si verifica un errore, devo propagare tale errore nell'intero mio codice, rendendo il codice molto più ingombrante (come mostrato negli esempi precedenti).
In realtà c'è un altro modo di infrangere il codice senza Exception
s usando return
(che di fatto è un altro "tabù" in Scala). Il codice sarebbe ancora più chiaro dell'approccio Either
e, pur essendo un po 'meno pulito dello stile Exception
, non ci sarebbe alcun timore di non catturato Exception
s.
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))
}
}
Sostanzialmente return Left
sostituisce throw new Exception
e il metodo implicito su entrambi, rightOrReturn
, è un supplemento per la propagazione automatica delle eccezioni nello stack.