Tak to prawda, nie wiem też jak by to miało działać np. ze zfailowaną transakcją na DB.
Zapewne dlatego to podejście pokryło się kurzem i zadne język nie planuje go wprowadzać, niemniej jako desing pattern fukcjonuje.
Tak to prawda, nie wiem też jak by to miało działać np. ze zfailowaną transakcją na DB.
Zapewne dlatego to podejście pokryło się kurzem i zadne język nie planuje go wprowadzać, niemniej jako desing pattern fukcjonuje.
Sam pomysł Checked Exceptions nie jest zły. A w czasach gdy projektowano Javę, takie mechanizmy były tworzone przez programistów za pomocą checkpointów. W COBOLu był zwykły RERUN ON CHEK
w FORTRANIE robiło się globalną flagę i warunek. Nie wspominam już o PL/SQL, bo tam mechanizm był mocno zależny od dostawcy, ale był. Często działało to tak, że wyjątek powodował cofnięcie do chekcpointu i wywołanie procedury „naprawczej”, która polegała na wywaleniu komunikatu z prośbą o ręczną weryfikację/naprawę danych. W Javie zrobiono to całkiem sensownie, bo wydzielono podzbiór wyjątków, które w teorii są „naprawialne”, jak brak pliku, czy połączenia z siecią, błędne dane wejściowe (ParseException
), czy nieprawidłowy sposób wywołania (IllegalXXXXException
). Problem polegał na tym, że ludzie zaczęli używać takich weryfikowalnych wyjątków w sposób podobny do przerwań lub jako sposób na zwrócenie wartości różnych typów.
0xmarcin napisał(a):
Tak to prawda, nie wiem też jak by to miało działać np. ze zfailowaną transakcją na DB.
Zapewne dlatego to podejście pokryło się kurzem i zadne język nie planuje go wprowadzać, niemniej jako desing pattern fukcjonuje.
A normalnie, ktoś loguje się do bazy danych i naprawia. Potem uruchamia program jeszcze raz. I kiedyś tak to działało, ale obecnie za dużo danych przeżuwamy, żeby ręcznie to ogarniać.
Gdyby tylko w Java można było używać generyków w throws
:
// nie działa
int foo() throws Err<String> { ... }
to można było by napisać generyczną metodę toEither(this::foo)
- czyli homomorfizm między throws & Either.
Teraz można zadać pytanie dla Either
owców? Jakich typów na błędy używacie w swoim Either? String
? Czy jakiś custom typ Error
?
Bo to jest dokładnie ten sam problem który występuje przy throws...
0xmarcin napisał(a):
Teraz można zadać pytanie dla
Either
owców? Jakich typów na błędy używacie w swoim Either?String
? Czy jakiś custom typError
?
Bo to jest dokładnie ten sam problem który występuje przy throws...
Ta odpowiedz to moze byc offtop, bo pewnie chodzi Ci stricte o Jave, ale w Scali 3 sa union types wiec mozna tak:
def create(input: FooInput): IO[ErrorA | ErrorB | ErrorC, Foo] = ???
// albo tak:
type FooCreationError = ErrorA | ErrorB | ErrorC
def create(input: FooInput): IO[FooCreationError, Foo] = ???
catch (IOException | NullPointerException e){}
//trollmode: off// Można tez torchę z generykami poszaleć, ale nie za dużo np. <T extends Runnable & Comparable<? super T>
powinno zadziałać (pisze z pamięci)
Cały czas ten sam problem że to są proste przykłady, weźmy ten:
def deserializeJson[T](json: String): IO[InvalidJson | InvalidDateFormat | InvalidNumberFormat, T] = ???
def fetchString(url: URL): IO[InvalidUrl | NetworkProblem | InvalidCertificate, String] = ???
// i teraz chcemy mieć funkcje to dostaje URL a zwraca obiekt sparsowany
def fetchObject[T](url: URL): IO[ /* co tutaj ??? */, T] = {
???
}
type DeserializationError = InvalidJson | InvalidDateFormat | InvalidNumberFormat
def deserializeJson[T](json: String): IO[DesarializationError, T] = ???
type FetchStringError = InvalidUrl | NetworkProblem | InvalidCertificate
def fetchString(url: URL): IO[FetchStringError, String] = ???
def fetchObject[T](url: URL): IO[DeserializationError | FetchStringError, T] = {
???
}
// albo idac dalej:
type FetchObjectError = DeserializationError | FetchStringError
def fetchObject[T](url: URL): IO[FetchObjectError, T] = {
???
}
// albo jesli jednak nie chcesz tego w ogole obslugiwac:
def fetchObject[T](url: URL): IO[Nothing, T] = {
???
}.orDie // robi z tego (unchecked) exception, ktore tez maja swoje miejsce w swiecie - ale tego nie ma sensu pozniej lapac, bo zamysl jest pewnie taki, zeby dalej nie obslugiwac requestu
// albo chcesz obsluzyc tylko niektore bledy:
def fetchObject[T](url: URL): IO[Nothing, T] = {
???
}.catchSomeOrDie { case desarializationError: DeserializationError => ??? }
CE jest spoko jesli blad jest integralna czescia sygnatury funkcji
i wtedy poszlo, ze ale to wtedy lepszy jest Either/Result
.orDie
)
Przy tym podejściu na realnej apce # typow błędow eksploduje
- nie nie eksploduje, mam całkiem duże aplikacje tak pisane w scali (duży zespół, lata pracy) i mam wrażenie, że typów błedów jest nawet mniej niż byłoby custom exceptionów w javie (prawdopodobnie nie jest mniej, ale po prostu zajmują dużo mniej linii kodu (hierarchia sealed class / case class vs ileś tam plików w javie).
Przetłumaczę jeszcze post @stivens na Java:
abstract class DeserializationError extends Exception { }
class InvalidJson extends DeserializationError { }
class InvalidDateFormat extends DeserializationError { }
class InvalidNumberFormat extends DeserializationError { }
<T> T deserializeJson(String json): throws DesarializationError { ??? }
abstract class FetchStringError extends Exception { }
class InvalidUrl extends DeserializationError { }
class NetworkProblem extends DeserializationError { }
class InvalidCertificate extends DeserializationError { }
String fetchString(URL url) throws FetchStringError { ??? }
// Nie ma typów algebraicznych w Java ale to nic, będziemy jechać na protezie
// (nie można użyć zwykłego Pari bo musi dziedziczyć po Exception):
class AnyOfErrors<T1, T2> extends Exception { // Trzeba mieć ten typ dla 2, 3, ..., 32 krotek
private final Object value;
}
// Znów hipotetycznie załóżmy że można użyć generyków w throws
<T> String fetchObject(URL url) throws AnyOf<DeserializationError, FetchStringError> = {
???
}
Java to jest jednak poj'bana. Dodali nowy interfejs czyli kolekcje sekwencyjne a tam metoda pobierz ostatni
default E getLast() {
if (this.isEmpty()) {
throw new NoSuchElementException();
} else {
return this.get(this.size() - 1);
}
}
Naprawdę skoro Java ma już tego kompniętego Optionala to nie można było zrobić
default Optional<E> getLastOpt() {
if (this.isEmpty()) {
return Optional()
} else {
return Optional(this.get(this.size() - 1));
}
}
Widać iż podniecanie się w Javie wyjątkami trwa w najlepsze i lepiej nie będzie :(
@KamilAdam obawiam się że to jest po prostu praca z legacy code. Inne metody api na kolekcji rzucały NoSuchElement a więc i nowa metoda musi też go rzucać.
Ujowo ale jednakowo jak mawiają w wojsku.
Natomiast to prawda że mogli by dodać też analogi które zwracają Optional. Sam o tym pisałem żeby unikać wyjątków kilka postów wcześniej...
Tego tematu nigdy nie zrozumiałem.
Zazwyczaj głosi się, że wyjątki, gdy aplikacja nie musi się crashować, wyjątki, które są obsługiwalne, to nigdy nie powinny być wyjątki, tylko Either.
Dlaczego to ma być Either, a nie wyjątki? Ano dlatego (jak rozumiem piewców Eithera), że Either jest kontrolowany przez system typów, a wyjątki nie. Jak jest Either, to klient API wie, że tu może być błąd, wręcz jest zmuszony ten błąd obsłużyć. A jak są wyjątki, no to błędy nagminnie nie są obsługiwane, bo bardzo łatwo zapomnieć, albo w ogóle nigdy się nie zorientować, że gdzieś tam dużo niżej coś może rzucić jakimś wyjątkiem. Dawniej byłem cięty na takie stanowisko, ale doświadczenie nauczyło mnie, ze ma ono dużo sensu.
No to mamy checked exceptions. One robią DOKŁADNIE to samo, co Either - możliwość wystąpienia błędu włączają do sygnatury metody, tak że nie da się tego przeoczyć.
Teraz z kolei krzyk, że checked exceptions to zuo.
Czy mógłby mi ktoś wytłumaczyć, w czym jest lepszy Either od checked exceptions?
Druga sprawa:
KamilAdam napisał(a):
Java to jest jednak poj'bana. Dodali nowy interfejs czyli kolekcje sekwencyjne a tam metoda pobierz ostatni
default E getLast() { if (this.isEmpty()) { throw new NoSuchElementException(); } else { return this.get(this.size() - 1); } }
Naprawdę skoro Java ma już tego kompniętego Optionala to nie można było zrobić
default Optional<E> getLastOpt() { if (this.isEmpty()) { return Optional() } else { return Optional(this.get(this.size() - 1)); } }
Widać iż podniecanie się w Javie wyjątkami trwa w najlepsze i lepiej nie będzie :(
A co miałby tu dać Optional??
To jest klasyczna sytuacja, gdy mamy błąd wynikający z winy programisty (bug, 'boneheaded exception') i aplikacja musi iść w dół. To jest błąd tej samej kategorii, jak próba pobrania elementu z nieistniejącego indeksu tablicy itp. Bez typów zależnych takich błędów się nie wyeliminuje.
Po co zmuszać programistę do obsługi takich błędów? Żeby za każdym razem, gdy programista jest PEWNY, że tablica jest niepusta i próbuje pobrać ostatni element musiał pisac ifa, że jeśli Optional nie ma wartości, to rzuć wyjątek "ProgrammerIsAnIdiotError"? Bo tego NIE DA SIĘ obsłużyć sensownie inaczej, jak tylko kładąc aplikację.
YetAnohterone napisał(a):
A co miałby tu dać Optional??
To iż sam mogę zdecydować jak to potem obsłużyć. Akurat w moim wypadku chcę wpisać tę wartość do arkusza więc muszę zamienić na stringa więc miałbym kod
list.getLastOpt().map(e -> e.tpString).getOrElse("");
To jest klasyczna sytuacja, gdy mamy błąd wynikający z winy programisty (bug, 'boneheaded exception') i aplikacja musi iść w dół. To jest błąd tej samej kategorii, jak próba pobrania elementu z nieistniejącego indeksu tablicy itp. Bez typów zależnych takich błędów się nie wyeliminuje.
Tu też może być zwracany Optional
Po co zmuszać programistę do obsługi takich błędów? Żeby za każdym razem, gdy programista jest PEWNY, że tablica jest niepusta i próbuje pobrać ostatni element musiał pisac ifa, że jeśli Optional nie ma wartości, to rzuć wyjątek "ProgrammerIsAnIdiotError"? Bo tego NIE DA SIĘ obsłużyć sensownie inaczej, jak tylko kładąc aplikację.
Jak faktycznie chcesz exception to nie musisz go rzucać. Czasem starczy
list.getLastOpt().get();
Poza tym nie wykluczam iż getLast()
nie może istnieć równolegle do getLastOpt()
. Jak akurat potrzebowałem getLastOpt()
i teraz musiałem ją sobie sam napisać
YetAnohterone napisał(a):
Zazwyczaj głosi się, że wyjątki, gdy aplikacja nie musi się crashować, wyjątki, które są obsługiwalne, to nigdy nie powinny być wyjątki, tylko Either.
Dlaczego to ma być Either, a nie wyjątki? Ano dlatego (jak rozumiem piewców Eithera), że Either jest kontrolowany przez system typów, a wyjątki nie. Jak jest Either, to klient API wie, że tu może być błąd, wręcz jest zmuszony ten błąd obsłużyć. A jak są wyjątki, no to błędy nagminnie nie są obsługiwane, bo bardzo łatwo zapomnieć, albo w ogóle nigdy się nie zorientować, że gdzieś tam dużo niżej coś może rzucić jakimś wyjątkiem. Dawniej byłem cięty na takie stanowisko, ale doświadczenie nauczyło mnie, ze ma ono dużo sensu.
Czy mógłby mi ktoś wytłumaczyć, w czym jest lepszy Either od checked exceptions?
Faktycznie checked exception jest częścią systemu typów w javie i zmusza do obsługi (tak jak Either).
Tylko, że jest to bardzo koślawy modyfikator typu, bo dostępny tylko dla funkcji (jako część typu funkcji).
W takim kodzie:
var x = mojaFunkcja()
innaFunkcja(x) //i co teraz?
jeśli jest oparty o checked exception to wyjątku nie przekażesz już jako rezultat do innaFunkcja
.
nawiązując do:
0xmarcin napisał(a):
Gdyby tylko w Java można było używać generyków w
throws
:// nie działa int foo() throws Err<String> { ... }
to można było by napisać generyczną metodę
toEither(this::foo)
- czyli homomorfizm między throws & Either.Teraz można zadać pytanie dla
Either
owców? Jakich typów na błędy używacie w swoim Either?String
? Czy jakiś custom typError
?
Bo to jest dokładnie ten sam problem który występuje przy throws...
oraz:
stivens napisał(a):
0xmarcin napisał(a):
Teraz można zadać pytanie dla
Either
owców? Jakich typów na błędy używacie w swoim Either?String
? Czy jakiś custom typError
?
Bo to jest dokładnie ten sam problem który występuje przy throws...Ta odpowiedz to moze byc offtop, bo pewnie chodzi Ci stricte o Jave, ale w Scali 3 sa union types wiec mozna tak:
def create(input: FooInput): IO[ErrorA | ErrorB | ErrorC, Foo] = ??? // albo tak: type FooCreationError = ErrorA | ErrorB | ErrorC def create(input: FooInput): IO[FooCreationError, Foo] = ???
to dla scali 3 oderski i świta pracują nad caprese, czyli typowanie i śledzenie zasobów i efektów bez monad transformerów czy innych złożonych typów:
około 36 minuty są obrazki porównujące caprese do rusta i innych języków:
ogólnie to nazywa się direct style
. virtual thready z javy 21+ to też direct style, ale nie jest uogólnione do śledzenia zasobów.
problem z checked exceptions (takimi jakie są w javie!, a nie np. w podanym wyżej caprese) jest to, że nie daje się tego abstrahować / komponować / etc. jeśli metoda przyjmuje tylko funkcje, która nie rzucają sprawdzanych wyjątków, a to co chcemy podać takie rzuca, to musimy przerobić checked exception na unchecked exception lub w ogóle zjeść exceptiona całkiem. weźmy pod uwagę taki hipotetyczny przykład. chcemy przemapować strumień stringów. javowy stream.map przyjmuje tylko funkcje, która nie rzucają checked exceptions, więc jeśli chcemy taką tam użyć, to musimy dodatkowo przepakować exception w taki unchecked albo zjeść ten exception albo zrobić coś innego, żeby już się w sygnaturze typu nie pojawiał.
problem z wyjątkami w ogóle jest taki, że np. nie są referentially transparent
. załóżmy, że jakaś metoda coś tam liczy i zwraca, ale przy okazji może rzucić wyjątek. teraz miejsce wywołania takiej metody ma znaczenie. jeśli wywołamy ją w bloku try-catch tam gdzie trzeba i zrobimy co trzeba to jest ok. jednak jeśli przeniesiemy wywołanie gdzieś, gdzie nie powinno być, np. poza try-catch, do callbacku, wyniesiemy poza warunkowe bloki kodu (a więc np. coś co wykonywało się czasem, będzie wykonywać się zawsze) to się okaże, że try-catch też trzeba przerobić, żeby nadal działał dobrze, ale kompilator nam tego nie podpowie, bo mamy przecież unchecked exception - coś co kompilator olewa.
eithery i inne monadki są ogólnie bezpieczne jeśli chodzi o refaktoring. można przenosić kod z miejsca na miejsce i nic nie powinno wybuchać (no chyba, że ktoś ma jakieś lewe monadki, albo używa monady typu identity
która nic nie opakowuje). jeśli typy się nie zgadzają to kompilator protestuje. jako, że eithery i inne monadki to generyczne opakowania, można je używać bezproblemowo w generycznych metodach.
mimo wszystko wyjątków i tak używam w pewnych przypadkach. główne zastosowanie to testy, bo testy piszę ofensywnie. inne zastosowanie to kod, który ma prosty przepływ sterowania, tzn. jest prosty zarówno faktycznie (nie ma żadnej asynchroniczności, leniwej ewaluacji i innych fajnych fikołków) jak i intuicyjnie (tzn. patrząc na nazwę i zastosowanie kodu domyślam się, że nie powinno być tam takich fajnych fikołków jak wymieniłem wcześniej, które sprawiają, że przepływ sterowania nie jest liniowy, a więc i zachowywałyby się nieobliczalnie w obliczu wyjątków).
w javie dawno nie kodowałem, ale z tego co pamiętam, to te checked exceptions były zwykle upierdliwą sztuką dla sztuki. nie zdążyłem poznać wyraźnych zalet tego rozwiązania :P . natomiast eithery w scali często są spoko. ładnie można sobie zamodelować błędy (prostą hierarchię typów), ładnie widać skąd te błędy przychodzą i kiedy się propagują (nawet jeśli są wpakowane np. w future'y), itp, itd.
koniec końców, dużo zależy od stylu programowania i problemu, który okodowujemy. ja tam nie mam podejścia skrajnego, tzn. nie obstaję twardo przy monadach w każdej sytuacji, ale z drugiej strony mocno je preferuję, więc programując w scali prawie zawsze są monadki, a wyjątki w kodzie produkcyjnym są bardzo rzadko.
Zarejestruj się i dołącz do największej społeczności programistów w Polsce.
Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.