Wyjątki, Either i programowanie funkcyjne

0

Ostatnio w książce 'Functional thinking' natknąłem się na stwierdzenie, że rzucanie wyjątków łamie dwie zasady programowani funkcyjnego:

  • powoduje side-effect, więc funkcja nie jest już 'pure'
  • powoduje, że tracimy 'referential transparency'

jednakże niektóre wątki na stackoverflow zdają się być sprzeczne z tym co Neal Ford opisał w swojej książce (np. ten https://stackoverflow.com/questions/10703232/why-is-the-raising-of-an-exception-a-side-effect).

Jak to jest z tym rzucaniem exceptionów - jest to dopuszczalne w programowaniu funkcyjnym czy nie? Jak to wygląda w praktyce - czy wprowadzenie Either albo czegos Either-podobnego zamiast try/catch daje wam jakiś zysk? Jest mniej błędów? można mieć spokojniejszy sen podczas oncalla?. Czy robicie to tylko po to, żeby poczuć wyższość nad ludźmi, którzy nie chcą wyjść poza świat OOP?

Całe życie byłem przyspawany do Javy i try/catch to było to dzięki czemu miałem pracę, ale zauważyłem, że sporo języków np. golang czy typescript pozwala na coś bardzo zbliżonego do Either, ale lepiej zintegrowane z językiem.

1

Zeby sie nie powtarzac, to wrzuce link: Są tu jeszcze jacyś miłośnicy checked exceptions? Używacie?

5

wprowadzenie Either albo czegos Either-podobnego zamiast try/catch daje wam jakiś zysk?

Podstawową zaletą Either (i podobnych, jak Result w Ruscie) jest to, że wymusza obsługę błędu, co prowadzi do tego, że zazwyczaj można spać spokojniej (bo wiemy, że wywołujący obsłużył jakoś błąd).

Czy robicie to tylko po to, żeby poczuć wyższość nad ludźmi, którzy nie chcą wyjść poza świat OOP?

Nie wiem co ma piernik do wiatraka, bo błędy jako zwracane wartości są zupełnie niezależne od OOP.

4

Źle współgra, ale to co piszą w tej książce to imo trochę bełkot. Rzucanie wyjątków nie prowadzi problemów, bo:

  • powoduje side-effect, więc funkcja nie jest już 'pure': nie widzę problemu, żeby funkcje rzucające wyjątki były pure. Jeśli założymy, że rzucony wyjątek jest pewnego rodzajem wynikiem funkcji to nie wiem, czemu div(0) -> Either<int> jest pure a div(0) -> int throws YouCannotDivideByZeroError już nie
  • powoduje, że tracimy 'referential transparency' jak wyżej możemy zastąpić każde wywołanie div(0) rzuceniem wyjątku YouCannotDivideByZeroError co spełnia referential transparency

Problemem jest łapanie wyjątków, bo samo łapanie we wszystkich znanych mi językach opiera się o statements, co nie jest funkcyjne. Nie widzę problemu, żeby napisać jakiegoś haskella od zera z javowymi wyjątkami jako taki "sugar". Nie ma to za bardzo sensu, ale nie widzę problemu dlaczego nie miałoby to działać

Jak to wygląda w praktyce - czy wprowadzenie Either albo czegos Either-podobnego zamiast try/catch daje wam jakiś zysk?

To po prostu inny typ obsługi błędów. Osobiście jestem za zwracaniem błędów, ale wyjątki też mają swoje zalety. Np. języki imperatywne operujące na zwracanych błędach (Rust, Go) i tak mają mechanizm do rzucania wyjątków (panic), które nie powinny być normalnie łapane. W językach z wyjątkami możesz użyć tego samego mechanizmu do obsługi obu rodzajów błędów, co jest eleganckie

że sporo języków np. golang ... ale lepiej zintegrowane z językiem.

heh (mówie to jako programista go).

czy wprowadzenie Either albo czegos Either-podobnego zamiast try/catch daje wam jakiś zysk?

Obsługa błędów ręcznie daje mnóstwo korzyści:

  • wymusza prostszy design, bo nie ma wyjątków rzucanych przez wiele funkcji
    • a jak są to wygląda to brzydko i człowiek musi się zastanowić, czy ma to sens
  • pozwala na traktowanie wyjątków jak wartości. Przykładowo mogę je sobie włożyć do jakiejś tablicy
  • wymusza obsługę błędów w każdym miejscu, co pozwala na lepiej przemyślany kod
  • działa lepiej z programowanie funkcyjnym
3
karellen napisał(a):

Czy robicie to tylko po to, żeby poczuć wyższość nad ludźmi, którzy nie chcą wyjść poza świat OOP?

Tak, na pewno o to chodzi. 😄

Zwracać obiekt opisujący błąd można też w języku obiektowym, i nie ma to za bardzo związku z wychodzeniem poza świat OOP. Ot, po prostu dzięki temu wiadomo, co się dzieje, i nie zachowujemy się jak psychopaci zbierający niepotrzebnie stacktrace.

0
karellen napisał(a):

Ostatnio w książce 'Functional thinking' natknąłem się na stwierdzenie, że rzucanie wyjątków łamie dwie zasady programowani funkcyjnego:

  • powoduje side-effect, więc funkcja nie jest już 'pure'
  • powoduje, że tracimy 'referential transparency'

W branży pokrewnej programowaniu funkcyjnemu jest tak że jak zaczynasz dostawać nieskończoności albo dzielić przez zero to znaczy że zrobiłeś coś źle. A jak zrobiłeś coś źle to cały tok jest o kant d. potłuc.

Dlatego książka ma jak najbardziej rację. Lepiej położyć program niż pozwolić czemuś takiemu obrażać logikę.

2

Mocno zależy jak się tych wyjątków używa. Programowanie funkcyjne opiera się na matematycznej definicji funkcji, czyli jakieś wyrażenie, które każdemu elementowi dziedziny jest w stanie przyporządkować dokładnie jeden element przeciwdziedziny. Czyli coś takiego:

fun reciprocal(x: Double): Double -> 1/x

Ponieważ dla 0 funkcja y=1/x jest nieokreślona, podczas wywołania reciprocal(0) dostaniemy wyjątek, DivideByZero który jeżeli nie zostanie wychwycony, wywali całą aplikację (a to trochę nie dobrze).
Natomiast jeżeli zaimplementujemu to w ten sposób:

fun reciprocal(x: Double): Either<Double, Undefined> -> when(x)(0 -> Eiher(Undefined) default: Either(1/x))

To po pierwsze coś, co nie było funkcją zaczyna być funkcją, po drugie programista korzystający z niej wie, że musi obsłużyć ścieżkę, albo świadomie jej nie obsłuży.

Teoretycznie da się coś takiego robić poprzez checked exception, ale praktyka wskazuje, że 99% tej obsługi wygląda tak:

try{...}
catch(Exception e){
  throw new RuntimeException(e);
}
0
piotrpo napisał(a):

To po pierwsze coś, co nie było funkcją zaczyna być funkcją, po drugie programista korzystający z niej wie, że musi obsłużyć ścieżkę, albo świadomie jej nie obsłuży.

Ale to jest przypadek trywialny. Co jeśli liczba wyjątków idzie w tysiące? Kto to ogarnie - no i po co?

0

No jest trywialny, bo po co tworzyć jakieś skomplikowane przykłady, skoro sens jest widoczny na podstawie prostego? Najzwyczajniej wygodnie jest używać funkcji, któa zawsze zwróci oczekiwaną wartość, niż takiej, która w jakichś tam przypadkach wrzuci odbezpieczony granat.

1
piotrpo napisał(a):

No jest trywialny, bo po co tworzyć jakieś skomplikowane przykłady, skoro sens jest widoczny na podstawie prostego? Najzwyczajniej wygodnie jest używać funkcji, któa zawsze zwróci oczekiwaną wartość, niż takiej, która w jakichś tam przypadkach wrzuci odbezpieczony granat.

Undefined to nie oczekiwana wartość. To jest inna enkapsulacja wyjątku. I za cholerę nie wiadomo co z tym zrobić - to jest przyznanie po stronie funkcji, ups, dostałem zestaw inputu który za cholerę nie przeze mnie kalkulowalny, baw się z tym dalej co prowadzi nas do punktu poniżej:

ale praktyka wskazuje, że 99% tej obsługi wygląda tak:

Który jest workaroundem na to że położenie aplikacji nie powinno mieć miejsca co jest bzdurą - logika natrafiła na coś co nie zostało w niej zamodelowane albo zostało zamodelowane z błędem - pozwolenie na dalsze działania czegoś takiego to jest etap machania rękami i liczenia na cud.

0

To jaka jest oczekiwana wartość dla wyrażenia 1/0? Bo w matematyce jest to właśnie wartość nieokreślona. Jeżeli piszesz kalkulator, gdzie użytkownik wpisuje 0, a następnie wciska guzik 1/x, to on powinien grzecznie użytkownika poinformować, że taka operacja matematyczna nie zwraca wyniku, a nie umierać.

0
piotrpo napisał(a):

To jaka jest oczekiwana wartość dla wyrażenia 1/0? Bo w matematyce jest to właśnie wartość nieokreślona. Jeżeli piszesz kalkulator, gdzie użytkownik wpisuje 0, a następnie wciska guzik 1/x, to on powinien grzecznie użytkownika poinformować, że taka operacja matematyczna nie zwraca wyniku, a nie umierać.

logika natrafiła na coś co nie zostało w niej zamodelowane albo zostało zamodelowane z błędem

Czy twoim zdaniem kalkulator nie zamodelował w tym wypadku 1/0? Bo jeśli nie to dalej nie wiem co chciałeś przekazać swoją odpowiedzią.

Żeby skrócić - Either to odpowiednik funkcji złożonej. Problem jest taki że o ile w czystej matematyce zazwyczaj input jest pod ściśłą kontrolą a cały model operuje na systemie zamkniętym to tego samego nie można powiedzieć o 99% aplikacji w IT.

Więc będziesz miał do czynienia albo z lawiną eskalacją wyjątków albo z lawinową eskalacją złożoności poprzez kompleksowe zagnieżdżanie funkcji które będą próbowały obsłużyć otwarte wejście (co z zasady jest niemożliwe). Tak czy siak skończysz albo z łapaniem wyjątków w jakiejś god-clasie albo z unmaintable mess w przypadku Either.

1
loza_prowizoryczna napisał(a):

albo z unmaintable mess w przypadku Either.

Co należy zrobić, żeby to osiągnąć?

5

No to wyszło, że nie da się napisać kalkulatora. Oczywiście błąd bierze się z tego, że dziedziny fun reciprocal(x: Double): Double -> 1/x oraz f(x) = 1/x są różne, bo Double to nie to samo co R =/= 0. Tylko co w związku z tym? Bo możliwości jakie widzę to:

  • Pogodzenie się z faktem, że w przypadku złych danych na wejściu aplikacja się wyzajączkuje. Niech się bambus uczy, że przez 0 się nie dzieli.
  • Walidacja danych na UI, ale wtedy UI musi wiedzieć, że przez 0 się nie dzieli, a to trochę tak jak gdyby na klawiaturę zrzucać odpowiedzialność za korektę ortografii.
  • Wprowadzić nowy typ danych, odpowiadający dziedzinie funkcji, np. RealExcept0. Tylko taki obiekt trzeba utworzyć, co oznacza przeniesienie problemu na poziom konstruktora/fabryuki tego obiektu, a to niekoniecznie jest "lepiej"
  • Przechwycić wyjątek na samej górze (UI) i obsłużenie go właśnie w tym miejscu. Tylko wysokie przechwytywanie wyjątków jak leci nie jest dobrą praktyką, bo po paru pięterkach wywołań ciężko już dojść dlaczego coś, gdzieś nisko poszło nie tak.
  • Zmiana typu zawracanego przez funkcję w taki sposób, żeby był w stanie poinformować, że funkcja jest nieokreślona dla wprowadzonej wartości.

Z tych opcji, żadna nie jest idealna, ale w moim przekonaniu ta ostatnia jest najbardziej sensowna.

0
somekind napisał(a):

Co należy zrobić, żeby to osiągnąć?

Popracować na realnym kodzie w języku gdzie typowanie błędów/wyjątków nie jest objawieniem na miarę Optionala

piotrpo napisał(a):
  • Zmiana typu zawracanego przez funkcję w taki sposób, żeby był w stanie poinformować, że funkcja jest nieokreślona dla wprowadzonej wartości.

Z tych opcji, żadna nie jest idealna, ale w moim przekonaniu ta ostatnia jest najbardziej sensowna.

Zgadzam się, lepiej wiedzieć że funkcja ma zachowanie undefined niż wiedzieć że zrobić throwa. Co prawda w typowym use casie oznacza to jedynie tyle że trzeba dopisać mniej testów ale w końcu programowanie piszemy przez programistów dla programistów.

0
loza_prowizoryczna napisał(a):

Popracować na realnym kodzie w języku gdzie typowanie błędów/wyjątków nie jest objawieniem na miarę Optionala

Pracuję na realnym kodzie, nie zauważam żadnej niezarządzalności.
Stąd moje pytanie - co trzeba spieprzyć, żeby wprowadzenie czegoś, co ułatwia pisanie kodu, go skomplikowało?

0
somekind napisał(a):

Pracuję na realnym kodzie, nie zauważam żadnej niezarządzalności.
Stąd moje pytanie - co trzeba spieprzyć, żeby wprowadzenie czegoś, co ułatwia pisanie kodu, go skomplikowało?

Skoro tak czytasz taski to muszę współczuć menedżerom prowadzącym ten projekt.

Odpowiadając - nie dyskutujemy na temat technik strukturyzacji wyjątków/errorów/scenariuszy nieprzewidzianych w modelu tylko na temat tego czy bezpieczniej w razie wyjątku położyć program czy pozwolić mu dalej działać nie będąc pewnym jak ten wyjątek obsłużyć.

To są podstawy modelowania systemów zamkniętych vs modelowanie systemów otwartych.

0
loza_prowizoryczna napisał(a):

Odpowiadając - nie dyskutujemy na temat technik strukturyzacji wyjątków/errorów/scenariuszy nieprzewidzianych w modelu tylko na temat tego czy bezpieczniej w razie wyjątku położyć program czy pozwolić mu dalej działać nie będąc pewnym jak ten wyjątek obsłużyć.

W przypadku wyjątku kładziemy program.

Ale my tu nie rozmawiamy o wyjątkach, tylko o błędach zwracanych przez funkcje.

0
somekind napisał(a):

Ale my tu nie rozmawiamy o wyjątkach, tylko o błędach zwracanych przez funkcje.

A czy błąd to nie inna nazwa na ładnie opakowany wyjątek?

1

Nie. Istnieje ogromna roznica miedzy np. blednym inputem od uzytkownika, a niedostepnym polaczeniem z baza danych

Albo cos co jest implementacja logiki biznesowej (tylko ze nie happy path), to tez ciezko nazwac wyjatkiem

0
stivens napisał(a):

Nie. Istnieje ogromna roznica miedzy np. blednym inputem od uzytkownika, a niedostepnym polaczeniem z baza danych

No brawo, czyli dochodzimy do sedna. Możemy sobie zamodelować interakcję z systemem zamkniętym ale wszystko idzie się j***ć jak mamy do czynienia z systemem otwartym. Jak w życiu.

Albo cos co jest implementacja logiki biznesowej, to tez ciezko nazwac wyjatkiem

Logika biznesowa opiera się na założeniu że jest systemem zamkniętym. A że w rzeczywistości jest kompletnie na odwrót to wie każdy z doświadczenia.

BTW: Jobs dlatego uznał że w ajfonach lepiej crash aplikacji pokazać jako jej zamknięcie do ekranu głównego niż na Androidzie gdzie apka rzucała ci w twarz komunikatem o crashu. Ten gość był bardziej łebski w zrozumieniu IT niż niektórzy chcieliby przyznać.

1

No i dlatego najlepiej wyjatki zostawic do sytuacji wyjatkowych, a faktyczne bledy traktowac jak bledy.

Jesli uderzasz do zewnetrznego API, to masz system otwarty, ale chwilowa niedostepnosc zewnetrznej uslugi jest raczej czyms "spodziewanym". Jesli uderzenie do tego API sie nie powiedzie, to zawsze mozna zrobic jakies retry policy (exponensial backoff np. albo w ogole uderzenie do konkurencyjnego API - np. kursy walutowe z innego banku)

0

Kolejna sprawa. Jak masz unchecked exception, to mozesz zapomniec to obsluzyc, albo w ogole nie miec w swiadomosci koniecznosci obsluzenia tego. Nie problem jesli, to jest tak krytyczne, ze faktycznie ma wylozyc aplikacje. Wiekszy jesli chcialbys to jednak obsluzyc.

Jak masz checked exceptions, to masz duzo bardziej ubogie i paskudne api/mechaniki do handlowania takich wyjatkow anizeli te udostepniane przez wartosci bledow (a przynajmniej przez te porzadne implementacje)

0
stivens napisał(a):

No i dlatego najlepiej wyjatki zostawic do sytuacji wyjatkowych, a faktyczne bledy traktowac jak bledy.

Jeśli outputem funkcji jest funkcja złożona either to nie masz błędu tylko jasne zachowanie. Problemem się pojawia gdy w 99% przypadków ta błędogenna część jest zestawem otwartym. Bo wtedy możesz się oszukiwać że obsłużyłeś błąd a resztę pchasz do góry w nadziei że ktoś inny go przejmie. Co w większości przypadków prowadzi do:

Jesli uderzasz do zewnetrznego API, to masz system otwarty, ale chwilowa niedostepnosc zewnetrznej uslugi jest raczej czyms "spodziewanym". Jesli uderzenie do tego API sie nie powiedzie, to zawsze mozna zrobic jakies retry policy (exponensial backoff np. albo w ogole uderzenie do konkurencyjnego API - np. kursy walutowe z innego banku)

Tak, wszystko przy założeniu że baza danych automagicznie po drugiej stronie API używając swoich automagicznych algorytmów zapewni nam spójność bo przecież jeśli API jest niedostępne dla nas to przecież musi być niedostępne dla innych. Bo w przeciwnym wypadku mógłby się pojawić rozjazd albo nawet niepoprawne naliczenie pewnych wartości (idempotentność, heheh).

A wtedy po prostu rzucimy użytkownikowi generyczny błąd na twarz i każdemy się skonsultować ze wsparciem albo zrobimy rollback licząc na to że zmiany wprowadzone przez użytkownika są tak małej wartości że nie będzie awanturował.

Powyższe to tylko ładniejsze opakowanie wywalenia aplikacji w starych systemach. Tylko tam system obsługiwali specjaliści więc jak coś się wywalało to się szukało problemu a dziś liczy się na to że otwarty input w postaci usera machnie na to ręką jak w życiu.

2

Zaczynasz gadac od rzeczy

T.j. piszesz duzo i tak zeby wygladalo madrze, ale nic z tego nie wynika. Ale to chyba standardowa technika trolli. * Nie mowie jeszcze, ze w tym watku trollujesz, ale kiedy kojarze Cie w ogolnosci z bycia quasi-trollem na tym forum, to trudno Cie na powaznie pozniej traktowac. Szczegolnie kiedy zaczynasz belkotac. ** No chyba ze Cie z kims pomylilem, to wtedy przepraszam ;)

0
stivens napisał(a):

Zaczynasz gadac od rzeczy

Jak się komuś zarzuca gadanie od rzeczy to trzeba wypunktować do rzeczy. A moje rzeczy są proste:

  • dyskusja jest jałowa bo wyjątek a błąd to praktycznie to samo tyle że ten drugi został zamodelowany a ten pierwszy wynika z skutków nieprzewidzianych (promieniowanie kosmiczne, błąd hardware'u whatever)
  • źródłem nieporozumienia jest jak zwykle Java która utożsamiła błąd zamodelowany z wyjątkiem (wszystko jest wyjątkiem) wobec czego całe sterowanie błędami zostało przerzucone na wyjątki (może są jakieś rozumne wyjątki, nie moja bajka). W założeniach Javy to że wszystko rzucić wyjątkiem ma sens bo większość programów operuje na systemach otwartych a prawdopodobieństwo złego zamodelowania logiki biznesowej jest wprost proporcjonalne do jej skomplikowania więc jest to rozsądne. To rzecz zostało zgwałcone i użyte do sterowanie przepływem zamodelowanych błędów już nie
  • odpowiadając więc wprost na pytanie OPa - mierność programistów Javy doprowadziła do tego że zwrócenie błędu jako częśći wyniku funkcji stało objawieniem języków funkcyjnych. Jak do tego doszło, pytaj Haskella.

EDIT: W Swifcie throws pod spodem tak naprawdę nie jest rzuceniem wyjątku tylko syntactic sugarem na stare dobre przekazanie **NSError z Obj-C. Wyjątki typu segfault, segdump zawsze i bez wyjątku kładą aplikację.

0

To da się napisać kalkulator, czy się nie da?
Java założenie do do wyjątków miała nawet OK, bo checked exceptions były rzucane z miejsc, gdzie problemy mogły się objawić problemy niezależne od aplikacji, np. zapis pliku. Tylko później się okazało, że jak ktoś bardzo nie chce obsłużyć takiej sytuacji, to jej nie obsłuży, a z drugiej strony pojawiły się pomysły na sterowanie wyjątkami i masz potworki typu Spring, gdzie jak użytkownik nie ma prawa wykonac jakiejś operacji, to rzucasz jakimś "Unauthorized", które później framework przekłada na HTTP401.

0
piotrpo napisał(a):

To da się napisać kalkulator, czy się nie da?

Da - ponieważ to system zamknięty o ograniczonej liczbie wyjątków (dzielenie przez zero) łatwy do zamodelowania. Co prawda niekoniecznie musi być spójny (arytmetyka Peano) ale skoro działa w 99% przypadków to znaczy że działa.

Java założenie do do wyjątków miała nawet OK,

I nikt z tym nie dyskutuje - w normalnym świecie każda operacja może zakończyć się failem. To nie matematyka tylko rzeczywistość. Problem zaczyna się taki że jak liczba tych operacji urasta do monstrualnych rozmiarów to kod odpowiedzialny za obsłużenie przewidzianych (i zamodelowanych) błędów również to w ostateczności kończysz z podejściem - chwytamy w tym miejscu wszystkie wyjątki i udajemy że nic się nie stało albo nie chwytamy i delegujemy to wyżej (co zazwyczaj kończy się wywaleniem aplikacji).

Syntactic sugar w postaci Either czyni rozróżnienie oczywistym ale to jak stwierdzenie że skoro wiemy że umrzemy to możemy się lepiej do tego przygotować.

0

Co prawda niekoniecznie musi być spójny (arytmetyka Peano) ale skoro działa w 99% przypadków to znaczy że działa

Czy Ty już zacząłeś Hell Win?😀 Przecież to nie ma sensu w swietle tego, że pracujemy na konkretnych reprezentacjach i algorytmach; poza tym chodzi o zupełność, nie spójność.

0
lion137 napisał(a):

Czy Ty już zacząłeś Hell Win?:smile

Tak ale jeszcze nigdy go nie skończyłem :(

Przecież to nie ma sensu w swietle tego, że pracujemy na konkretnych reprezentacjach i algorytmach;

No i to jest problem z konkretnymi reprezentacjami - na fladze kolor wolności, rewolucji i monarchii a w szczególe sama czerń. Jeśli o algorytmy to się nie wypowiem bo którykolwiek nie zastosuję to praktycznie zawsze co jakiś czas dostanę wynik niezgodny z książkowym. Ja tu ufać czemuś takiemu?

poza tym chodzi o zupełność, nie spójność.

Ciężko udowodnić spójność w niezupełności.

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.