Non molto tempo fa ho iniziato a utilizzare Scala anziché Java. Per me, parte del processo di "conversione" tra le lingue stava imparando a usare Either
s anziché (controllato) Exception
s. Ho programmato in questo modo per un po ', ma recentemente ho iniziato a chiedermi se è davvero un modo migliore per andare.
Uno dei maggiori vantaggi Either
è Exception
rappresentato dalle prestazioni migliori; è Exception
necessario creare una traccia stack e viene lanciata. Per quanto ne capisco, però, lanciare la parte Exception
non è impegnativo, ma costruire lo stack-trace è.
Ma poi, si può sempre costruire / ereditare Exception
s scala.util.control.NoStackTrace
, e ancora di più, vedo molti casi in cui il lato sinistro di un Either
è in realtà un Exception
(rinunciare all'aumento delle prestazioni).
Un altro vantaggio Either
è la sicurezza del compilatore; il compilatore Scala non si lamenterà di Exception
s non gestiti (diversamente dal compilatore Java). Ma se non sbaglio, questa decisione è motivata con lo stesso ragionamento che viene discusso in questo argomento, quindi ...
In termini di sintassi, mi sembra che lo Exception
stile sia molto più chiaro. Esaminare i seguenti blocchi di codice (entrambi raggiungendo 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 mi sembra molto più pulito, e il codice che gestisce l'errore (o pattern-matching su Either
o try-catch
) è abbastanza chiaro in entrambi i casi.
Quindi la mia domanda è: perché usare Either
oltre (selezionato) Exception
?
Aggiornare
Dopo aver letto le risposte, mi sono reso conto che avrei potuto non riuscire a presentare il nucleo del mio dilemma. La mia preoccupazione non riguarda la mancanza di try-catch
; si può "catturare" un Exception
con Try
o usare il catch
per avvolgere l'eccezione con Left
.
Il mio problema principale con Either
/ Try
viene quando scrivo codice che potrebbe non riuscire in molti punti lungo la strada; in questi scenari, quando si verifica un errore, devo propagare quell'errore in tutto il mio codice, rendendo il codice molto più ingombrante (come mostrato negli esempi di cui sopra).
Esiste in realtà un altro modo per violare il codice senza Exception
usare return
(che in realtà è un altro "tabù" in Scala). Il codice sarebbe ancora più chiaro diEither
dell'approccio e, pur essendo un po 'meno pulito dello Exception
stile, non ci sarebbe paura di non essere catturati Exception
.
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))
}
}
Fondamentalmente i return Left
rimpiazzi throw new Exception
e il metodo implicito su entrambi,rightOrReturn
, è un supplemento per la propagazione automatica delle eccezioni nello stack.
Try
. La parte su Either
vs Exception
afferma semplicemente che Either
s dovrebbe essere usato quando l'altro caso del metodo è "non eccezionale". Innanzitutto, questa è una definizione molto, molto vaga. Secondo, vale davvero la pena della sintassi? Voglio dire, non mi dispiacerebbe davvero usare Either
s se non fosse per la sintassi ambientale che presentano.
Either
mi sembra una monade. Usalo quando hai bisogno dei vantaggi della composizione funzionale che offrono le monadi. O forse no .
Either
per sé non è una monade. La proiezione sul lato sinistro o destro è una monade, ma di Either
per sé non lo è. Puoi renderla una monade, "tendendola" al lato sinistro o destro, però. Tuttavia, impartisci un certo semantico su entrambi i lati di un Either
. La Scala Either
era originariamente imparziale, ma era di parte piuttosto di recente, quindi oggigiorno è in realtà una monade, ma la "monadness" non è una proprietà intrinseca di, Either
ma piuttosto il risultato di essere di parte.