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 Eithers anziché (controllato) Exceptions. 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è Exceptionrappresentato dalle prestazioni migliori; è Exceptionnecessario creare una traccia stack e viene lanciata. Per quanto ne capisco, però, lanciare la parte Exceptionnon è impegnativo, ma costruire lo stack-trace è.
Ma poi, si può sempre costruire / ereditare Exceptions 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 Exceptions 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 Exceptionstile 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 Eithero try-catch) è abbastanza chiaro in entrambi i casi.
Quindi la mia domanda è: perché usare Eitheroltre (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 Exceptioncon Tryo usare il catchper avvolgere l'eccezione con Left.
Il mio problema principale con Either/ Tryviene 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 Exceptionusare 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 Exceptionstile, 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 Leftrimpiazzi throw new Exceptione il metodo implicito su entrambi,rightOrReturn , è un supplemento per la propagazione automatica delle eccezioni nello stack.
Try. La parte su Eithervs Exceptionafferma semplicemente che Eithers 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 Eithers se non fosse per la sintassi ambientale che presentano.
Eithermi sembra una monade. Usalo quando hai bisogno dei vantaggi della composizione funzionale che offrono le monadi. O forse no .
Eitherper sé non è una monade. La proiezione sul lato sinistro o destro è una monade, ma di Eitherper 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 Eitherera originariamente imparziale, ma era di parte piuttosto di recente, quindi oggigiorno è in realtà una monade, ma la "monadness" non è una proprietà intrinseca di, Eitherma piuttosto il risultato di essere di parte.