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.
źródło
Try
. W części dotyczącejEither
vsException
podano jedynie, żeEither
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życiuEither
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, aleEither
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 stronachEither
. ScalaEither
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ń.Odpowiedzi:
Jeśli użyjesz tylko bloku
Either
imperatywnegotry-catch
, będzie to oczywiście wyglądać jak inna składnia, aby zrobić dokładnie to samo.Eithers
są wartością . Nie mają takich samych ograniczeń. Możesz:Eithers
nie musi znajdować się tuż obok części „try”. Może być nawet współdzielony we wspólnej funkcji. To znacznie ułatwia prawidłowe rozdzielanie problemów.Eithers
w strukturach danych. Możesz je przeglądać i konsolidować komunikaty o błędach, znajdować pierwszy, który nie zawiódł, leniwie je oceniać lub podobnie.Eithers
? Możesz napisać funkcje pomocnicze, aby ułatwić ich obsługę w określonych przypadkach użycia. W przeciwieństwie do try-catch, nie utkniesz na stałe tylko z domyślnym interfejsem wymyślonym przez projektantów języka.Uwaga dla większości przypadków użycia,
Try
ma prostszy interfejs, ma większość powyższych zalet i zwykle spełnia również twoje potrzeby. JestOption
to jeszcze prostsze, jeśli liczy się tylko sukces lub porażka i nie potrzebujesz żadnych informacji o stanie, takich jak komunikaty o błędach dotyczące przypadku awarii.Z mojego doświadczenia
Eithers
nie są tak naprawdę preferowane,Trys
chyba żeLeft
jest to coś innego niżException
, być może komunikat o błędzie sprawdzania poprawności, i chcesz agregowaćLeft
wartości. Na przykład przetwarzasz niektóre dane wejściowe i chcesz zebrać wszystkie komunikaty o błędach, a nie tylko błąd pierwszego. Takie sytuacje nie zdarzają się często w praktyce, przynajmniej dla mnie.Spójrz poza model try-catch, a będziesz o
Eithers
wiele przyjemniejszy w pracy.Aktualizacja: to pytanie wciąż przyciąga uwagę i nie widziałem aktualizacji OP wyjaśniającej pytanie składniowe, więc pomyślałem, że dodam więcej.
Jako przykład wyjścia z myślenia wyjątkowego, tak zazwyczaj napisałbym twój przykładowy kod:
Tutaj widać, że semantyka
filterOrElse
idealnego uchwycenia tego, co robimy przede wszystkim w tej funkcji: sprawdzanie warunków i kojarzenie komunikatów o błędach z danymi awariami. Ponieważ jest to bardzo częsty przypadek użycia,filterOrElse
znajduje się w standardowej bibliotece, ale gdyby tak nie było, można go utworzyć.To jest punkt, w którym ktoś wymyślił kontrprzykład, którego nie da się rozwiązać dokładnie tak samo jak mój, ale próbuję nadmienić, że nie ma tylko jednego sposobu użycia
Eithers
, w przeciwieństwie do wyjątków. Istnieją sposoby na uproszczenie kodu w większości sytuacji związanych z obsługą błędów, jeśli zapoznasz się ze wszystkimi funkcjami dostępnymiEithers
i jesteś otwarty na eksperymenty.źródło
try
icatch
jest uczciwym argumentem, ale czy nie można tego łatwo osiągnąćTry
? 2. PrzechowywanieEither
w strukturach danych może odbywać się równolegle zthrows
mechanizmem; czy nie jest to po prostu dodatkowa warstwa nad błędami w obsłudze? 3. Zdecydowanie możesz rozszerzyćException
s. Jasne, nie możesz zmienićtry-catch
struktury, ale obsługa awarii jest tak powszechna, że zdecydowanie chcę, aby język dał mi do tego podstawową płytę (której nie jestem zmuszony używać). To tak, jakby powiedzieć, że niezrozumienie jest złe, ponieważ stanowi podstawę dla operacji monadycznych.Either
można rozszerzyć, tam gdzie wyraźnie nie mogą (sąsealed trait
tylko dwie implementacje, obafinal
). Czy możesz to rozwinąć? Zakładam, że nie miałeś na myśli dosłownego znaczenia rozszerzenia.Oba są bardzo różne.
Either
Semantyka jest znacznie bardziej ogólna niż tylko reprezentowanie potencjalnie nieudanych obliczeń.Either
pozwala zwrócić jeden z dwóch różnych typów. Otóż to.Teraz jeden z tych dwóch typów może , ale nie musi, reprezentować błąd, a drugi może , ale nie musi, reprezentować sukces, ale jest to tylko jeden z możliwych przypadków użycia wielu.
Either
jest o wiele bardziej ogólny niż to. Równie dobrze możesz mieć metodę, która odczytuje dane wejściowe od użytkownika, który może albo wprowadzić nazwę, albo zwrócić datęEither[String, Date]
.Lub pomyśl o literałach lambda w C♯: albo oceniają na an
Func
lubExpression
, tzn. Albo oceniają na anonimową funkcję, albo oceniają na AST. W C♯ dzieje się to „magicznie”, ale jeśli chcesz nadać mu odpowiedni typ, byłoby to możliweEither<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>
. Jednak żadna z tych dwóch opcji nie jest błędem i żadna z dwóch nie jest ani „normalna”, ani „wyjątkowa”. Są to tylko dwa różne typy, które mogą zostać zwrócone z tej samej funkcji (lub w tym przypadku przypisane do tego samego literału). [Uwaga: w rzeczywistościFunc
musi być podzielony na dwa kolejne przypadki, ponieważ C♯ nie maUnit
typu, więc coś, co niczego nie zwraca, nie może być reprezentowane w ten sam sposób, co coś, co zwraca,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>
Teraz
Either
można go użyć do przedstawienia typu sukcesu i typu niepowodzenia. A jeśli zgodzisz się na spójne uporządkowanie tych dwóch typów, możesz nawet stronniczośćEither
preferować jeden z tych dwóch typów, umożliwiając w ten sposób rozprzestrzenianie awarii poprzez łańcuch monadyczny. Ale w takim przypadku przekazujesz pewien semantycznyEither
fakt, że nie musi on sam w sobie posiadać.Ten,
Either
który ma jeden z dwóch typów ustalony jako typ błędu i jest stronniczy w celu propagowania błędów, jest zwykle nazywanyError
monadą i byłby lepszym wyborem do przedstawienia potencjalnie nieudanego obliczenia. W Scali ten rodzaj jest nazywanyTry
.Znacznie bardziej sensowne jest porównywanie użycia wyjątków z
Try
niżEither
.Jest to zatem powód nr 1, dlaczego
Either
można go stosować w stosunku do sprawdzonych wyjątków:Either
jest znacznie bardziej ogólny niż wyjątki. Może być stosowany w przypadkach, w których wyjątki nawet nie mają zastosowania.Najprawdopodobniej jednak pytałeś o ograniczony przypadek, w którym po prostu używasz
Either
(lub bardziej prawdopodobneTry
) jako zamiennika wyjątków. W takim przypadku nadal istnieją pewne zalety, a raczej możliwe różne podejścia.Główną zaletą jest to, że można odroczyć obsługę błędu. Po prostu przechowujesz wartość zwracaną, nawet nie sprawdzając, czy jest to sukces, czy błąd. (Poradzę sobie później.) Lub przekaż to gdzie indziej. (Niech ktoś się tym martwi.) Zbierz je na liście. (Obierz je wszystkie naraz.) Możesz użyć łańcuchów monadycznych, aby propagować je wzdłuż potoku przetwarzania.
Semantyka wyjątków jest wbudowana w język. W Scali, na przykład, wyjątki rozwijają stos. Jeśli zezwolisz im na przeniesienie się do procedury obsługi wyjątku, program obsługi nie może wrócić do miejsca, w którym się zdarzyło, i naprawić. OTOH, jeśli złapiesz go niżej na stosie przed rozwinięciem go i zniszczeniem niezbędnego kontekstu, aby go naprawić, możesz znajdować się w komponencie, który jest na zbyt niskim poziomie, aby podjąć świadomą decyzję o tym, jak postępować.
Inaczej jest w Smalltalk, na przykład, gdy wyjątki nie odprężyć stosu i są wznawialną i można złapać wyjątek wysoko w górę w stosie (ewentualnie tak wysoko jak debugger), naprawić błąd waaaaayyyyy dół w stos i wznów wykonywanie stamtąd. Lub warunki CommonLisp, w których podnoszenie, łapanie i obsługa to trzy różne rzeczy zamiast dwóch (podnoszenie i chwytanie), jak w wyjątkach.
Użycie rzeczywistego typu zwracanego błędu zamiast wyjątku pozwala robić podobne rzeczy i więcej, bez konieczności modyfikacji języka.
A potem w pokoju jest oczywisty słoń: wyjątki są efektem ubocznym. Skutki uboczne są złe. Uwaga: nie mówię, że to koniecznie prawda. Ale jest to punkt widzenia niektórych osób, w takim przypadku przewaga typu błędu nad wyjątkami jest oczywista. Tak więc, jeśli chcesz wykonać programowanie funkcjonalne, możesz użyć typów błędów tylko z tego powodu, że są to podejście funkcjonalne. (Pamiętaj, że Haskell ma wyjątki. Pamiętaj też, że nikt nie lubi ich używać).
źródło
Exception
odpowiedzi.Either
wydaje się świetnym narzędziem, ale tak, konkretnie pytam o obsługę awarii i wolę użyć do mojego zadania bardziej szczegółowego narzędzia niż wszechmocnego. Odroczenie obsługi błędów jest słusznym argumentem, ale można po prostu użyćTry
metody rzucającej i,Exception
jeśli nie chce się w tej chwili z tym obchodzić. Czy odwijanie śledzenia stosu nie wpływa również naEither
/Try
? Możesz powtórzyć te same błędy, niepoprawnie je propagując; wiedza, kiedy poradzić sobie z awarią, nie jest trywialna w obu przypadkach.Zaletą wyjątków jest to, że kod źródłowy sklasyfikował już wyjątkową okoliczność. Różnica między
IllegalStateException
a aBadParameterException
jest znacznie łatwiejsza do odróżnienia w silniejszych językach pisanych. Jeśli spróbujesz parsować „dobre” i „złe”, nie korzystasz z niezwykle potężnego narzędzia, które masz do dyspozycji, a mianowicie kompilatora Scala. Własne rozszerzeniaThrowable
oprócz tekstu ciąg wiadomości mogą zawierać tyle informacji, ile potrzebujesz.Jest to prawdziwa użyteczność
Try
w środowisku Scala,Throwable
ponieważ działa jak opakowanie na lewe przedmioty, aby ułatwić życie.źródło
Either
ma problemy ze stylem, ponieważ nie jest to prawdziwa monada (?); arbitralnośćleft
wartości odwraca się od wyższej wartości dodanej, gdy odpowiednio zawrzesz informacje o wyjątkowych warunkach w odpowiedniej strukturze. Tak więc porównanie użyciaEither
w jednym przypadku zwracania ciągów po lewej stronie,try/catch
w drugim przypadku użycie poprawnie wpisanego tekstuThrowables
nie jest właściwe. Ponieważ wartościThrowables
za dostarczanie informacji oraz sposób zwięzły, żeTry
uchwytyThrowables
,Try
jest to lepsze niż albo ...Either
albotry/catch
try-catch
. Jak powiedzieliśmy w pytaniu, ten ostatni wygląda dla mnie o wiele czystiej, a kod obsługujący błąd (albo dopasowywanie wzorca na Either albo try-catch) jest dość jasny w obu przypadkach.