Nie tak dawno temu zacząłem używać Scali zamiast Javy. Częścią procesu „konwersji” między językami była dla mnie nauka używania Either
s zamiast (zaznaczonych) Exception
s. Kodowałem w ten sposób od jakiegoś czasu, ale ostatnio zacząłem się zastanawiać, czy to naprawdę lepszy sposób.
Jedną z głównych korzyści Either
jest przez Exception
to lepsze wyniki; Exception
musi zbudować stosu śladu i jest wyrzucane. O ile rozumiem, rzucanie Exception
nie jest wymagającą częścią, ale budowanie śladu stosu jest.
Ale wtedy zawsze można skonstruować / dziedziczą Exception
sz scala.util.control.NoStackTrace
, a jeszcze bardziej, widzę mnóstwo przypadków, gdzie lewa strona Either
jest w rzeczywistości Exception
(rezygnując z zwiększenie wydajności).
Kolejną zaletą Either
jest bezpieczeństwo kompilatora; kompilator Scala nie będzie narzekał na nieobsługiwane Exception
s (w przeciwieństwie do kompilatora Java). Ale jeśli się nie mylę, ta decyzja jest uzasadniona takim samym uzasadnieniem, jak omawiane w tym temacie, więc ...
Pod względem składni wydaje mi się, że Exception
-style jest o wiele jaśniejszy. Sprawdź następujące bloki kodu (oba osiągają tę samą funkcjonalność):
Either
styl:
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
styl:
@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 = ???
Ta ostatnia wygląda dla mnie o wiele czystiej, a kod obsługujący awarię (albo dopasowanie wzorca na Either
lub try-catch
) jest dość jasny w obu przypadkach.
Więc moje pytanie brzmi - po co używać Either
(sprawdzone) Exception
?
Aktualizacja
Po przeczytaniu odpowiedzi zdałem sobie sprawę, że mogłem nie przedstawić sedna mojego dylematu. Moim zmartwieniem nie jest brak try-catch
; można albo „złapać” za Exception
pomocą Try
, albo użyć catch
do owinięcia wyjątku Left
.
Mój główny problem z Either
/ Try
pojawia się, gdy piszę kod, który może zawieść w wielu punktach po drodze; w tych scenariuszach, gdy napotykam awarię, muszę propagować tę awarię w całym moim kodzie, przez co kod jest znacznie bardziej kłopotliwy (jak pokazano we wspomnianych przykładach).
Istnieje inny sposób na złamanie kodu bez użycia Exception
s return
(co w rzeczywistości jest kolejnym „tabu” w Scali). Kod byłby jeszcze bardziej przejrzysty niż Either
podejście, a chociaż byłby nieco mniej czysty niż Exception
styl, nie obawiałby się, że nie zostanie złapany 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))
}
}
Zasadniczo return Left
zamienia throw new Exception
i niejawna metoda na obu rightOrReturn
, jest dodatkiem do automatycznej propagacji wyjątków w górę stosu.
Try
. W części dotyczącej Either
vs Exception
podano jedynie, że Either
należy użyć, gdy drugi przypadek metody jest „nietypowy”. Po pierwsze, jest to bardzo, bardzo niejasne imho definicja. Po drugie, czy naprawdę jest warta kary składni? To znaczy, naprawdę nie miałbym nic przeciwko użyciu Either
s, gdyby nie narzut, jaki prezentują składnie.
Either
dla mnie wygląda jak monada. Używaj go, gdy potrzebujesz funkcjonalnych korzyści z kompozycji, jakie zapewniają monady. A może nie .
Either
sam w sobie nie jest monadą. Projekcja albo do lewej lub prawej stronie jest monada, ale Either
sama w sobie nie jest. Możesz zrobić z niego monadę, „odchylając” ją jednak do lewej lub prawej strony. Jednak wtedy przekazujesz pewien semantyczny po obu stronach Either
. Scala Either
była pierwotnie bezstronna, ale ostatnio była tendencyjna, tak że obecnie jest w rzeczywistości monadą, ale „monadność” nie jest nieodłączną właściwością, Either
ale raczej wynikiem uprzedzeń.