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 Eithers zamiast (zaznaczonych) Exceptions. 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 Eitherjest przez Exceptionto lepsze wyniki; Exceptionmusi zbudować stosu śladu i jest wyrzucane. O ile rozumiem, rzucanie Exceptionnie jest wymagającą częścią, ale budowanie śladu stosu jest.
Ale wtedy zawsze można skonstruować / dziedziczą Exceptionsz scala.util.control.NoStackTrace, a jeszcze bardziej, widzę mnóstwo przypadków, gdzie lewa strona Eitherjest w rzeczywistości Exception(rezygnując z zwiększenie wydajności).
Kolejną zaletą Eitherjest bezpieczeństwo kompilatora; kompilator Scala nie będzie narzekał na nieobsługiwane Exceptions (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 Eitherlub 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 Exceptionpomocą Try, albo użyć catchdo owinięcia wyjątku Left.
Mój główny problem z Either/ Trypojawia 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 Exceptions return(co w rzeczywistości jest kolejnym „tabu” w Scali). Kod byłby jeszcze bardziej przejrzysty niż Eitherpodejście, a chociaż byłby nieco mniej czysty niż Exceptionstyl, 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 Leftzamienia throw new Exceptioni niejawna metoda na obu rightOrReturn, jest dodatkiem do automatycznej propagacji wyjątków w górę stosu.
Try. W części dotyczącej Eithervs Exceptionpodano jedynie, że Eithernależ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 Eithers, gdyby nie narzut, jaki prezentują składnie.
Eitherdla mnie wygląda jak monada. Używaj go, gdy potrzebujesz funkcjonalnych korzyści z kompozycji, jakie zapewniają monady. A może nie .
Eithersam w sobie nie jest monadą. Projekcja albo do lewej lub prawej stronie jest monada, ale Eithersama 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 Eitherbył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ą, Eitherale raczej wynikiem uprzedzeń.