Dlaczego w jednych językach spamowanie wyjątków jest akceptowalne, a w innych nie?

1

Dla tego w językach dynamicznie typowanych fajny jest pattern matching. Przykładowo by wyciągnąć z mapy wartość:

{:ok, value} = Map.fetch(map, key)

Tak, to rzuci wyjątkiem, jeśli klucza nie ma, ale to nie Map.fetch/2 będzie źródłem wyjątku a niespasowanie przy matchowaniu, jeśli chcemy "poprawnie" obsłużyć sytuację gdy wartości nie ma to:

case Map.fetch(map, key) do
  {:ok, value} -> # do something with `value`
  :error -> # key do not exist
end
0
hauleth napisał(a):
{:ok, value} = Map.fetch(map, key)

Tak, to rzuci wyjątkiem, jeśli klucza nie ma, ale to nie Map.fetch/2 będzie źródłem wyjątku a niespasowanie przy matchowaniu, jeśli chcemy "poprawnie" obsłużyć sytuację gdy wartości nie ma to:

case Map.fetch(map, key) do
  {:ok, value} -> # do something with `value`
  :error -> # key do not exist
end

No dobra, ale czym to się będzie różnić od Pythonowego try/exept z wyjątkiem KeyError? Dla mnie (z opisu) działa tak samo, tylko kod inaczej wygląda (bo inny język)?

0

@TurkucPodjadek: w zależności co zastosujesz. 1 przypadek stosujesz gdy masz praktycznie pewność, że key występuje w map, aczkolwiek lepiej wtedy użyć pattern matchingu na samej mapie w formie

%{^key => value} = map

Jeśli masz coś takiego to zakładasz, że na 100% nie będzie sytuacji, gdzie klucza w mapie nie ma, a jak już się taka sytuacja zdarzy (z jakiegoś niewyjaśnionego powodu) tzn., że cały proces (w VMce) ma się wyłożyć i zostać zrestartowany, bo coś jest bardzo nie tak (i trzymamy kciuki, że to był tylko jednorazowy problem).

W drugim przypadku będzie to zdecydowanie szybsze niż wyjątek, bo nie trzeba "odwijać" stosu, co w wielu językach jest bardzo kosztowną operacją.

0

IMO to zależy głównie od architektury (jak zawsze), a dopiero potem od techstacku. Ja np. mam jedną apkę w pythonie, w której baza pilnuje konsystencji danych, bo po to właśnie tam jest. Więc jak zajdzie mi taka sytuacja, to zwyczajnie loguję taki error i program leci sobie dalej (bo nic się nie stanie jak w mojej mapie nie będzie klucza, będzie duplikat, lub coś z kosmosu), nie bawię się w zbawianie sytuacji w kodzie, bo INSERT się nie powiódł.

Za to mam apkę, która korzysta z dystrybuowanej bazy danych, gdzie baza się tym tak już nie przejmuje bardzo i część roboty w podtrzymaniu konsystencji danych odwala właśnie sama apka. Tam w takiej sytuacji oczywiście leci panic, aby mi bazy nie popsuło, bo wywalenie się tej apki, to jest dla mnie zdecydowanie mniejszy problem od zepsutej bazy danych (nawet jak mam jej świeży backup).

1

Sorry za odgrzewanie tematu sprzed pół roku, ale czytam go jeszcze raz i dalej nie do końca rozumiem.

jarekr000000 napisał(a):

Jak wyjątek da się sensownie obsłużyć... to nie jest wyjątkiem i wtedy obrabiamny go Eitherem, czy czymkolwiek podobnym. No i wyszło, że takie podejście sprawdza się również w Javie, Kotlinie i Scali.

jarekr000000 napisał(a):
kmph napisał(a):

Czemu nie w Pythonie?

Bo trzeba mnieć system typów / type checker. Bez tego cały koncept siada. Kompilator wymusza wtedy na programiście obsługę sytuacji wyjątkowych - troszkę tak jak w javie wymuszone jest obsługiwanie checked exceptions, tylko mechanizm jest bardziej ogólny i mniej magiczny.
Wprowadzając eithery, optionale, io itp do języka bez type checkera zwiększamy sobie tylko ilość potencjalnych problemów, bo trzeba pilnować i pamiętać w co zapakowany jest rezultat.

OK - w językach statycznie typowanych wyjątki są nieoptymalne, jeśli można je obsłużyć, bo wyrzucamy do śmieci korzyści ze statycznego typowania. Jakaś funkcja dużo niżej od nas może cośtam rzucić, więc przydałoby się cośtam obsłużyć, ale nie wiemy co, bo kompilator nam nie podpowiada. Z kolei jeśli dostajemy Eithery, Optionaly, czy coś w tym guście, to od razu po typie wartości zwracanej przez funkcję, którą wołamy, wiemy, na co musimy zwracać uwagę i jakie sytuacje musimy obsługiwać. Jeśli wyjątku nie da się obsłużyć, to oczywiście nie ma to znaczenia, więc można użyć wyjątku. Także w językach dynamicznie typowanych to nie ma sensu, bo i tak kompilator nic nam nie podpowie i jesteśmy zdani tylko na siebie. Ma to sens.

Ale przecież w Javie, jak sam zauważyłeś (a w przeciwieństwie do np. C#) JEST statyczne sprawdzanie wyjątków przez kompilator! A właśnie m.in. w Javie jest ten zakaz rzucania wyjątków, które nie mają scrashować całej apki. Wracamy więc do punktu wyjścia - jaka jest w Javie przewaga alternatywnych mechanizmów nad wyjątkami? Pozornie przynajmniej wyjątki wydają się być mniej więcej identyczne w funkcjonalności do Eithera, bo jedno i drugie podlega statycznemu sprawdzaniu i jedno i drugie pozwala powiedzieć dokładnie, w czym jest problem. Tylko że w Either trzeba chyba samemu dzielnie budować strukturę danych określającą, co się w ogóle stało, w wyjątkach nie trzeba.

Shalom napisał(a):

Either dla mnie to odpowiednik Checked Exceptions w javie, czyli takich wyjątków które trzeba deklarować i obsługiwać. Praktycznie zawsze można zamienić jedno na drugie. Tylko ze problem z nimi jest taki, że zwykle chcemy je obsługiwać "od razu" i nie bardzo ma sens robienia z tego wyjątku, który przerywa nam control-flow.
Wyjątki runtime to są faktycznie sytuacje wyjątkowe, które wymagają wyskoczenia wiele poziomów w górę bo zwyczajnie nic się nie da zrobić.

Czyli po prostu konwencja? Jedno i drugie robi to samo, ale przyjęło się, że wyjątek to koniec świata, a Either da się sensownie obsłużyć? Chodzi tu więc o to, by nie skonfudować kogoś, kto czyta kod, a nie obiektywne wady/zalety tej czy innej konstrukcji?

==============================================================================================================================

Powyższe było do sytuacji możliwej do sensownego obsłużenia - a teraz na temat sytuacji niemożliwej do sensownego obsłużenia:

Shalom napisał(a):

Jednoczeście uważałbym z patrzeniem "jak to jest zrobione w XYZ" bo inżynieria oprogramowania cały czas sie rozwija. To co kiedyś wydawało sie OK, mogło się nie sprawdzić i dziś state of the art jest inne. Np. w javie jak robisz get na mapie i nie ma takiego klucza to dostajesz nulla, podczas gdy dziś oczywistym byłoby zwrócenie pustego optionala.

Ale często jest tak, że jeśli robimy get na mapie i nie ma takiego klucza, to znaczy, że gdzieś jest bug w kodzie. A skoro tak, to co możemy zrobić z tym pustym optionalem? Nic mądrzejszego, tylko propagować błąd wyżej - dalej zwrócić pustego optionala. Aż w końcu ten pusty optional dojdzie do maina. A main wyświetla błąd Programmer is an idiot error i zakańcza działanie programu. ... A zatem... Właśnie zduplikowaliśmy funkcjonalność wyjątków? Jedyna różnica jest taka, że niemalże wszystko w kodzie mamy opakowane optionalami.

0

Ale często jest tak, że jeśli robimy get na mapie i nie ma takiego klucza, to znaczy, że gdzieś jest bug w kodzie. A skoro tak, to co możemy zrobić z tym pustym optionalem? Nic mądrzejszego, tylko propagować błąd wyżej - dalej zwrócić pustego optionala. Aż w końcu ten pusty optional dojdzie do maina. A main wyświetla błąd Programmer is an idiot error i zakańcza działanie programu. ... A zatem... Właśnie zduplikowaliśmy funkcjonalność wyjątków? Jedyna różnica jest taka, że niemalże wszystko w kodzie mamy opakowane optionalami.

Nigdy mi się nie zdarzyło propagować pustego Optionala do maina, to bez sensu. Można go propagować np do odpowiedzi z endpointa RESTowego (np przy pustym Optionalu zwracamy 404 nie znaleziono czegoś tam). Równie dobrze mógłbyś napisać - co możemy zrobić z nullem? Nic mądrzejszego tylko propagować go wyżej, aż dojdzie do maina? Nieee, nulle się sprawdza i na nie reaguje.

OK - w językach statycznie typowanych wyjątki są nieoptymalne, jeśli można je obsłużyć, bo wyrzucamy do śmieci korzyści ze statycznego typowania. Jakaś funkcja dużo niżej od nas może cośtam rzucić, więc przydałoby się cośtam obsłużyć, ale nie wiemy co, bo kompilator nam nie podpowiada. Z kolei jeśli dostajemy Eithery, Optionaly, czy coś w tym guście, to od razu po typie wartości zwracanej przez funkcję, którą wołamy, wiemy, na co musimy zwracać uwagę i jakie sytuacje musimy obsługiwać. Jeśli wyjątku nie da się obsłużyć, to oczywiście nie ma to znaczenia, więc można użyć wyjątku. Także w językach dynamicznie typowanych to nie ma sensu, bo i tak kompilator nic nam nie podpowie i jesteśmy zdani tylko na siebie. Ma to sens.

Ale przecież w Javie, jak sam zauważyłeś (a w przeciwieństwie do np. C#) JEST statyczne sprawdzanie wyjątków przez kompilator! A właśnie m.in. w Javie jest ten zakaz rzucania wyjątków, które nie mają scrashować całej apki. Wracamy więc do punktu wyjścia - jaka jest w Javie przewaga alternatywnych mechanizmów nad wyjątkami? Pozornie przynajmniej wyjątki wydają się być mniej więcej identyczne w funkcjonalności do Eithera, bo jedno i drugie podlega statycznemu sprawdzaniu i jedno i drugie pozwala powiedzieć dokładnie, w czym jest problem. Tylko że w Either trzeba chyba samemu dzielnie budować strukturę danych określającą, co się w ogóle stało, w wyjątkach nie trzeba.

Java ma checked exceptions, Kotlin, C# czy Scala nie mają. Nie wszystkie wyjątki są checked, wiele nie jest: https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html Nawet w przypadku gdy są to bardzo często się te szczegółowe typy zlewa i wstawia throws IOException czy nawet throws Exception i wiadomo tyle, że coś co nie dziedziczy po java.lang.RuntimeException może być rzucone. Dodatkowo checked exceptions, nawet z precyzyjnymi typami, są nieprecyzyjne jeśli chodzi o wskazywanie na źródło checked exception. throws SomeCheckedException daje się na poziomie całej metody i nie widać od razu z którego wywołania pochodzi. Mając Eithera widzimy gdzie jest zwracany, więc dużo łatwiej prześledzić gdzie był stworzony ("rzucony"). Dzięki temu, że Either jest zwykłą klasą tak jak wszystkie inne, a nie wyspecjalizowanym mechanizmem możemy tymi Eitherami operować jak innymi klasami. Dla przykładu mogę stworzyć listę Eitherów, albo np stworzyć dwie mapy Map<Key, Either<Problem, Result>> i je złączyć (na zasadzie, że jeśli klucze się pokrywają to jakoś tam, wedle uznania, łączymy odpowiadające im Eithery).

Czyli po prostu konwencja? Jedno i drugie robi to samo, ale przyjęło się, że wyjątek to koniec świata, a Either da się sensownie obsłużyć? Chodzi tu więc o to, by nie skonfudować kogoś, kto czyta kod, a nie obiektywne wady/zalety tej czy innej konstrukcji?

W Scali mam trochę inne kryteria. try/catch używam zwykle:

  • gdy chcę złapać konkretny wyjątek i wyprodukować z niego Eithera (tzn nie chcę przez przypadek łapać wyjątków, których nie przewidziałem). W innym przypadku robię Try(kod), który jest takim generycznym Eitherem łapiącym wszystkie niekrytyczne wyjątki (w sensie scala.util.control.NonFatal)
  • gdy w środku try niewiele się dzieje (nie ma łapania wyjątku z głębokich wywołań, itd), a kod wygląda dużo lepiej niż z Either/ Try
  • gdy chcę zrobić wymuszanie jakiejś reakcji (np System.exit) zamiast polegać na domyślnej obsłudze nieobsłużonych wyjątków (np co mi z tego, że pół apki się sypnie, a drugie pół będzie działać? lepiej ubić całość od razu)

Ogólnie w kodzie Scalowym, mimo iż programuję mocno obiektowo (tzn nie jestem funkcyjnym purystą korzystającym z zio/scalaz/cats/etc) to try/catch się dość rzadko pojawia. Być może nawet try/finally pojawia się częściej niż try/catch.

To co się przydaje przy Eitherach to aliasy dla typów. Java tego nie ma, ale ma to Scala (i też np TypeScript czy Kotlin). Dla przykładu mogę zrobić:

type MyResult[A] = Map[SomeClassWithLongName, Either[SomeReallySpecificError, A]]

i od tej pory pisać MyResult[String] zamiast Map[SomeClassWithLongName, Either[SomeReallySpecificError, String]]. W pewnych przypadkach mocno skraca to kod.

Wracając do checked exceptions to mają one bardzo kiepską opinię, w przeciwieństwie do unchecked exceptions czy Eitherów o których opinia jest na pewno lepsza. Jeśli ktoś odkryje sens w checked exceptions to będzie musiał przekonać cały Javowy świat, by do tych checked exceptions powrócił i ich nie wywalał przy każdej możliwej okazji.

0
Wibowit napisał(a):

Równie dobrze mógłbyś napisać - co możemy zrobić z nullem? Nic mądrzejszego tylko propagować go wyżej, aż dojdzie do maina? Nieee, nulle się sprawdza i na nie reaguje.

Tam, gdzie się nulla oczekuje, tam się go sprawdza i nań reaguje. ALe chyba nie powiesz mi, że za każdym razem, gdy cokolwiek czytamy, zawsze obowiązkowo sprawdzamy nulla, nawet tam, gdzie nulla nie ma prawa być? No a jeśli nulla nie ma prawa być, a mimo wszystko jest, to znaczy że mamy bug więc leci wyjątek. Jak inaczej zareagować na sytuację, gdy właśnie wykryliśmy buga podczas działania programu?

Nie wszystkie wyjątki są checked, wiele nie jest: https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html

Eric Lippert dawno temu podzielił wyjątki na 4 kategorie: Fatal, Boneheaded, Vexing i Exogenous. Fatal i Boneheaded zazwyczaj powinny crashować całą aplikację, choć z odmiennych przyczyn. Zdaje mi się, że przy tej klasyfikacji RuntimeException w Javie zazwyczaj wpadałyby do kategorii Fatal albo Boneheaded właśnie? Czyli nic nie tracimy na tym, że ich nie sprawdzamy statycznie, bo one i tak mają scrashować apkę.

Nawet w przypadku gdy są to bardzo często się te szczegółowe typy zlewa i wstawia throws IOException czy nawet throws Exception i wiadomo tyle, że coś co nie dziedziczy po java.lang.RuntimeException może być rzucone.

No ale to wina programisty, a nie konstrukcji języka.

Dzięki temu, że Either jest zwykłą klasą tak jak wszystkie inne, a nie wyspecjalizowanym mechanizmem możemy tymi Eitherami operować jak innymi klasami. Dla przykładu mogę stworzyć listę Eitherów, albo np stworzyć dwie mapy Map<Key, Either<Problem, Result>> i je złączyć (na zasadzie, że jeśli klucze się pokrywają to jakoś tam, wedle uznania, łączymy odpowiadające im Eithery).

OK. (chociaż w sumie można by Map<Key, Either<Exception, Result>>) - czemu wyjątek nie ma mówić o problemie? Nie musimy ograniczać się do jednego mechanizmu

Dodatkowo checked exceptions, nawet z precyzyjnymi typami, są nieprecyzyjne jeśli chodzi o wskazywanie na źródło checked exception. throws SomeCheckedException daje się na poziomie całej metody i nie widać od razu z którego wywołania pochodzi. Mając Eithera widzimy gdzie jest zwracany, więc dużo łatwiej prześledzić gdzie był stworzony ("rzucony").

Niby można każde wywołanie opatrywać try/catch...

Ale to nie jest tego rodzaju sytuacja, w której to zakaz używania wyjątków wydaje mi sie być mniej wygodny.

Tam, gdzie (mi się wydaje) wyjątki są wygodne, to wtedy, gdy trzeba wyskoczyć gdzieś wyżej, nawet jeśli owo "wyżej" może błąd sensownie obsłużyć.

Zalinkowany już przeze mnie Lippert jako przykład złego wyjątku (Vexing) daje parsowanie stringów do intów. No ale właśnie parsowanie inputu usera wydaje mi się być przykładem, gdzie wyjątki są po prostu wygodne...

public MyDataFormat ParseUserInput(string userInput)
{
    var (first, middle, last) = BreakInput(userInput);
    return new MyDataFormat(ParseFirstPartOfInput(first), Int32.Parse(middle), ParseLastPartOfInput(last));
}

I jak gdziekolwiek coś pójdzie nie tak, to wyjątek. Trzeba tylko pamiętać, by ten wyjątek obsłużyć w kodzie wołającym ParseUserInput.

public bool TryParseUserInput(string userInput, out MyDataFormat result)
{
    if(TryBreakInput(userInput, out (string first, string middle, string last) t))
    {
        var (first, middle, last) = t;
        if(TryParseFirstPartOfInput(first, out var firstParsed) && Int32.TryParse(middle, out int middleParsed) && TryParseLastPartOfInput(last, out var lastParsed))
        {
            result = new MyDataFormat(firstParsed, middleParsed, lastParsed);
            return true;
        }
    }
    return false;
}

A TryBreakInput, TryParseFirstPartOfInput, TryParseLastPartOfInput i ew. metody jeszcze niżej będą wyglądały tak samo, jak ten potworek!!

(aczkolwiek 1. Uznaję, że podejście z wyjątkami łatwo pokoci wyjątki powstałe w wyniku bugu w ParseUserInput i wyjątki powstałe w wyniku błędnych danych wejściowych - bo sensownie jest uznać, że jeśli leci JAKIKOLWIEK wyjątek, to z pewnością w wyniku błędnych danych; z drugiej strony, rozróżnienie między tymi dwoma źródłami wyjątków wymagałoby upstrzenia kodu mnóstwem try/catch, co wyglądąłoby równie brzydko; 2. być może na Eitherach robi się to tak samo wygodnie jak na wyjątkach, szczególnie w języku do tego przeznaczonym, jak Haskell; 3. być może da się uprościć moje TryParse bez wprowadzania wyjątków)

0

gdzie wyjątki są po prostu wygodne (...) I jak gdziekolwiek coś pójdzie nie tak, to wyjątek. Trzeba tylko pamiętać, by ten wyjątek obsłużyć w kodzie wołającym

właśnie dla tego tylko 'potworek' jest lepszy. Człowiek ma problem zapamiętać 9 znakowy numer telefonu. Nie opłaca się marnować cennego ram'u, na myslenie o dodaniu try catch za każdym razem gdy wołasz xyz. Drugą wadą jest border-plate. W przypadku paternu bool trySomething(out result) sytuacje można ocenić jednym rzutem oka, bez udziału świadomego myślenia, bo wszystko wynika z sygnatury i konwencji. A border-plate to jeden wynikający z istoty rzeczy if i smutna zmienna dla out. Zwracanie optionali ma te podobne zalety.

public MyDataFormat ParseUserInput(string userInput) Wyjątek tutaj ma sens ale nie dlatego ze jest wygodniejszy ale dlatego że nie można zwrócić MyDataFormat i trzeba jakoś te informacje przekazać. Natomiast sama metoda jest wygodniejsza, pod warunkiem że zwalidowaliśmy dane poziom wyżej i unikamy duplikacji/borderplate'u

0

Tam, gdzie się nulla oczekuje, tam się go sprawdza i nań reaguje. ALe chyba nie powiesz mi, że za każdym razem, gdy cokolwiek czytamy, zawsze obowiązkowo sprawdzamy nulla, nawet tam, gdzie nulla nie ma prawa być? No a jeśli nulla nie ma prawa być, a mimo wszystko jest, to znaczy że mamy bug więc leci wyjątek. Jak inaczej zareagować na sytuację, gdy właśnie wykryliśmy buga podczas działania programu?

Analogicznie są sytuacje kiedy Optional nie ma sensu i się nim nie opakowuje wyniku. Nieprzewidziane wyjątki się jednak zdarzają i to w każdym języku. Nawet w C++ dostajesz np segfaulty i niekoniecznie je łapiesz, chociaż teoretycznie mógłbyś: https://code.google.com/archive/p/segvcatch/

Eric Lippert dawno temu podzielił wyjątki na 4 kategorie: Fatal, Boneheaded, Vexing i Exogenous. Fatal i Boneheaded zazwyczaj powinny crashować całą aplikację, choć z odmiennych przyczyn. Zdaje mi się, że przy tej klasyfikacji RuntimeException w Javie zazwyczaj wpadałyby do kategorii Fatal albo Boneheaded właśnie? Czyli nic nie tracimy na tym, że ich nie sprawdzamy statycznie, bo one i tak mają scrashować apkę.

Z RuntimeException dziedziczy masa wyjątków. Do crashowania apki mają służyć podklasy java.lang.Error która tak samo jak java.lang.RuntimeException też jest unchecked.

W artykule napisano:

Vexing exceptions are the result of unfortunate design decisions. Vexing exceptions are thrown in a completely non-exceptional circumstance, and therefore must be caught and handled all the time.

The classic example of a vexing exception is Int32.Parse, which throws if you give it a string that cannot be parsed as an integer.

Tak się składa, że java.lang.NumberFormatException (rzucany w java.lang.Integer.parseInt) jest unchecked bo dziedziczy po java.lang.RuntimeException. Stąd od razu mam przykład vexing exception który jest unchecked w Javie.

No ale to wina programisty, a nie konstrukcji języka.

I tak i nie. Winą języka jest np to, że to throws Litania, Różnych, Wyjątków trzeba powtarzać jeśli przepychamy checked exceptions przez kilka poziomów. Teoretycznie można zrobić:

try {
  ... jakiś kod
} catch (Litania | Różnych | Wyjątków exception) {
  throw new WspólnyException(exception);
}

i mieć jeden typ wyjątku do przepychania, ale zrobienie throws Exception jest dużo bardziej kuszące. Ogólnie programiści będą robić tak jak im wygodniej, o ile jacyś guru im nie wyperswadują żeby robić inaczej. Dla przykładu Javowcy namiętnie piszą akcesory nawet jeśli nie mają sensu, ale taki jest branżowy "standard". Checked exceptions natomiast nie jest postrzegane jako branżowy standard tylko jako porażka. Możesz z tym walczyć, ale prawdopodobnie będziesz jak Don Kichot.

OK. (chociaż w sumie można by Map<Key, Either<Exception, Result>>) - czemu wyjątek nie ma mówić o problemie? Nie musimy ograniczać się do jednego mechanizmu

Można łączyć Eithery z wyjątkami, ale nie zawsze ma to sens. Np kodzie if (a < 0) return Either.left("wrong value") można by stworzyć wyjątek zamiast pakować gołego Stringa do Eithera, ale po co?

W Scali są for-comprehensions, więc wyciąganie wartości z wielu eitherów jest dość wygodne: https://docs.scala-lang.org/overviews/scala-book/functional-error-handling.html W Javie tego nie ma, więc trzeba inaczej kombinować. Od dawna nie kodzę w Javie (tzn kodzę, ale sporadycznie hobbystycznie), więc nawet nie wiem jak wielki to jest problem.

2

Dorzucę swoje trzy grosze, być może to uprości. W Javie widzę to obecnie tak:

  • Checked Exceptions : Powinien być używany gdy autor API chce dać wołającemu możliwość naprawienia stanu i ponowienia operacji. W zasadzie nie powinien być używany w nowym API, ponieważ można je zastąpić Optional, Option lub Either. Sytuacja, że możemy coś naprawić i ponowić jest rzadko spotykana w praktyce.
  • Runtime Exceptions : Powinny być używane w sytuacjach gdy program nie może kontynuować działania. Konieczna jest jakaś akcja (tj. naprawienie kodu, naprawienie środowiska, naprawienie stanu). Takie wyjątki zawsze powinny być widoczne w monitoringu i budzić nas w nocy.

Rzeczy takie jak walidacja danych, dzielnik równy 0 nie są sytuacjami wyjątkowymi. To normalne, że użytkownik może podać zły email czy IBAN. Programowanie tego za pomocą wyjątków jest słabe ponieważ:

  • rzucanie wyjątków w znaczącej ilości przypadków wymaga zbudowania stack trace - kosztowna operacja,
  • wyjątki są traktowane jako statystycznie mało prawdopodobne i zwykle słabo optymalizowane przez JIT.
    Lepiej użyć klas z Vavr jak Either lub własny obiekt.

Dochodzi wam do sytuacji, że dużo użytkowników zaczyna wpisywać niepoprawne dane, mamy duży load i dodatkowo zabijamy serwis rzucając wyjątki.

No chyba, że zakładamy iż niepoprawne dane nie powinny nigdy się pojawić, bo odpowiada za to GUI, wówczas możemy rzucać wyjątek jako znak, że mamy błąd w kodzie GUI lub ktoś próbuje hackować aplikację. Przy takich założeniach nieprawne dane są sytuacją wyjątkową, jednak zwykle piszemy jakieś REST-y i nie wiemy jaka aplikacja tego używa.

Takie podejście upraszcza myślenie. Po prostu gdy widzę jakiś wyjątek to od razu wiem, że o czymś ja lub kolega zapomnieliśmy i zaczynam działać. Nie zastanawiam się czy ten wyjątek to jest tutaj by design czy nie

Wideo w tym temacie:

0

Ale często jest tak, że jeśli robimy get na mapie i nie ma takiego klucza, to znaczy, że gdzieś jest bug w kodzie. A skoro tak, to co możemy zrobić z tym pustym optionalem? Nic mądrzejszego, tylko propagować błąd wyżej - dalej zwrócić pustego optionala. Aż w końcu ten pusty optional dojdzie do maina. A main wyświetla błąd Programmer is an idiot error i zakańcza działanie programu. ... A zatem... Właśnie zduplikowaliśmy funkcjonalność wyjątków? Jedyna różnica jest taka, że niemalże wszystko w kodzie mamy opakowane optionalami.

To zależy co ta mapa przechowuje.
Może być tak że będziemy chcieli zwrócić jakąs defaultową wartość więc wtedy zrobimy getOrElse(defaultValue);
Może być to jakaś mapa np. z jakąś konfiguracją która powinna być żeby aplikacja działała, wtedy dostajemy NoSuchElementException ale to jest już raczej wyjątkowa sytuacja.
Np. W Javie mapa ma metodę getOrDefault czyli tak jak Optional orElse, a Optional Javovy ma w przewadze metodę orElseGet która przymuje suppliera (a jeśli mamy duży koszt obsługi orElse to warto zastosować)

2

Tak długo, jak społeczność programistyczna nie dorośnie do tego, aby rozwijać języki, które wykorzystują Dependent Types tak długo patologia programistyczna w postaci budowania logiki aplikacji na wyjątkach, czyli sterowanie przypływem na zasadzie "współczesnego Go To", będzie kontynuowana. Wyjątki służą do zatrzymania aplikacji w kontekście problemu spowodowanego przez programistę i to nie jest charakterystyka jakiegoś konkretnego języka. A Java to w ogóle jakiś dziwny stan umysłu, w którym najważniejsze jest to co mówią aktualnie inflenserzy na YT. Jak widzę języki imperatywne (mieszane...) z Eitherami - jest lepiej, ale oczy bolą od patrzenia na ten syntax. W ScalaZ jeszcze jakoś to wygląda, ale i tak zawsze znajdzie się jakiś geniusz Grand Sinior, który będzie puszczał wyjątki z Optionala.

2
Anonnas napisał(a):

Tak długo, jak społeczność programistyczna nie dorośnie do tego, aby rozwijać języki, które wykorzystują Dependent Types ...

Możesz rozwinąć jak mają wpływać Dependent Types na ograniczenie budowania aplikacji na wyjątkach?

1
Anonnas napisał(a):

Wyjątki służą do zatrzymania aplikacji w kontekście problemu spowodowanego przez programistę i to nie jest charakterystyka jakiegoś konkretnego języka.

Jednak, o ile mi wiadomo, w Pythonie obowiązuje przeciwna zasada, która głosi ask for forgiveness not permission. Nawet oficjalna dokumentacja Pythona zaleca spamonie blokami try/catch.

Chyba, że sens Twojej wypowiedzi jest taki, że po prostu uznajesz jedynie czysto funkcyjne języki z systemem typów tak silnym, jak to tylko możliwe (ideał - niech typ funkcji starczy za formalny dowód jej poprawności?) - więc każdy krok w tym kierunku jest pozytywny? Ale w takim razie Python powinien dla Ciebie być syntezą wszelkiego zła?

Czy źle rozumiem Twoją wypowiedź?

1

Ja tam najbardziej cenię podejście Erlanga:

Wyjątki się zdarzają, w każdej aplikacji, prędzej czy później. Naszym zadaniem nie jest zagwarantowanie, że nigdy one się nie zdarzą, bo to jest niemożliwe (błędy programistów, złe dane wejściowe, promieniowanie kosmiczne). Zamiast skupmy się na tym, by ograniczyć obszar zniszczenia jak takowy wystąpi. W takim przypadku możemy podzielić błędy na 2 rodzaje:

  1. Jebło, to jebło. Nie ma co drążyć. Ubijmy subsystem (proces Erlanga), zalogujmy to, spróbujmy ponownie i może tym razem się uda. To ma sens w dużej ilości przypadków, bo np. nie ma sensu męczyć się z błędnym zapytaniem HTTP tylko ubijmy cały handler dla tego konkretnego zapytania i zwróćmy błąd użytkownikowi. Jak będzie trzeba to sam powtórzy operację/odświeży stronę. Ogólnie - programista zapewne spieprzył, ale spokojnie, ograniczyliśmy zniszczenia.
  2. Możemy się spodziewać, że dane nie do końca mogą być poprawne i możemy się z tego wyratować w jakiś sposób. Wtedy zwracamy krotkę i niech programista sam ogarnie co z tym zrobić.

Oczywiście całkiem często te dwie rzeczy się wzajemnie przeplatają, ale ze względu na 1. to nie ma aż takiego znaczenia o ile problem występuje dość rzadko (gdzie "dość rzadko" jest konfigurowalne). Jeśli występuje częściej niż trzeba to przenosimy problem wyżej tak długo aż sama VM nie stwierdzi, że jednak nie bangla i wtedy się wykłada z impetem.

0
hauleth napisał(a):

Ja tam najbardziej cenię podejście Erlanga:

Wyjątki się zdarzają, w każdej aplikacji, prędzej czy później. Naszym zadaniem nie jest zagwarantowanie, że nigdy one się nie zdarzą, bo to jest niemożliwe (błędy programistów, złe dane wejściowe, promieniowanie kosmiczne). Zamiast skupmy się na tym, by ograniczyć obszar zniszczenia jak takowy wystąpi. W takim przypadku możemy podzielić błędy na 2 rodzaje:

  1. Jebło, to jebło. Nie ma co drążyć. Ubijmy subsystem (proces Erlanga), zalogujmy to, spróbujmy ponownie i może tym razem się uda. To ma sens w dużej ilości przypadków, bo np. nie ma sensu męczyć się z błędnym zapytaniem HTTP tylko ubijmy cały handler dla tego konkretnego zapytania i zwróćmy błąd użytkownikowi. Jak będzie trzeba to sam powtórzy operację/odświeży stronę. Ogólnie - programista zapewne spieprzył, ale spokojnie, ograniczyliśmy zniszczenia.
  2. Możemy się spodziewać, że dane nie do końca mogą być poprawne i możemy się z tego wyratować w jakiś sposób. Wtedy zwracamy krotkę i niech programista sam ogarnie co z tym zrobić.

Oczywiście całkiem często te dwie rzeczy się wzajemnie przeplatają, ale ze względu na 1. to nie ma aż takiego znaczenia o ile problem występuje dość rzadko (gdzie "dość rzadko" jest konfigurowalne). Jeśli występuje częściej niż trzeba to przenosimy problem wyżej tak długo aż sama VM nie stwierdzi, że jednak nie bangla i wtedy się wykłada z impetem.

Tylko, że ja znowu nie rozumiem tego "podejścia". W czasach Dockera i k8s każdy głupi język programowania to ma, nawet nie tylko to, bo można określić zaawansowane zależności, w stylu, że apka ma wstać/położyć się jak np. cache leży, ale baza działa i zadziała to we wszystkich stackach odpalanych na tych platformach. Zatem wynoszenie tej "codziennej" zalety uważam za coś dziwnego, albo ja czegoś tu nie rozumiem z tym Erlangiem (?)

0

W czasach Dockera i k8s każdy głupi język programowania to ma, nawet nie tylko to

Tak, ale nie do końca. Docker oferuje Ci to na poziomie systemu operacyjnego, tak samo jak każdy inny init-system (bogowie jak ja nie cierpię tej współczesnej "dokeryzacji" wszystkiego co się da). Ok, położy Ci się cała aplikacja i po chwili w stanie, ale z racji, że to proces systemowy to to z reguły trochę trwa. Chyba, że odpalasz nowy kontener Dockera dla każdego osobnego połączenia HTTP. Erlang oferuje bardziej podejście jak FaaS niż sam Docker.

bo można określić zaawansowane zależności

Nie do takiego stopnia jak w Erlangu. W Dokerze zależności masz w większości "płaskie". W Erlangu mogę zbudować drzewo zależności, gdzie na różnych poziomach mam różne zachowania w przypadku błędu. Na ten przykład mam "głównego" supervisora, który działa na zasadzie "rest for one", czyli jeśli jedno dziecko umrze to wszystkie "młodsze" niż on są ubijane i restartowane. W związku z czym np. jak DB leży to zrestartuje się również serwer HTTP czy inne elementy. Natomiast samo połączenie z DB działa na zasadzie "one for one" gdzie jeśli jedno połączenie padło, to zostanie zastąpione nowym, bez ingerencji w pozostałe. Daje nam to ogromne możliwości nie tylko na kontrolę procesów, ale również na ograniczanie rażenia błędów. Już pomijam tutaj sprawy jak monitory czy rozproszony supervising przez co programista może "transparentnie" traktować cały klaster maszyn Erlanga jak jedną maszynę (bo komunikacja opiera się na wysyłaniu wiadomości).

Zatem wynoszenie tej "codziennej" zalety uważam za coś dziwnego, albo ja czegoś tu nie rozumiem z tym Erlangiem (?)

Parę rzeczy:

  • Erlang jest starszy od Dockera, Erlang jest starszy od Linuksa (o 5 lat). Przez ten czas naprawdę wiele problemów już zauważono i rozwiązano
  • Erlang de facto to soft-real-time preemptive multiprocess OS, w którym bardzo do serca wzięto sobie stabilność systemu (dziewięć dziewiątek stabilności - https://www.rabbitmq.com/resources/armstrong.pdf strona 27)
  • Procesy Erlanga są zdecydowanie "tańsze" niż kontenery Dockera, można też je odpalać szybciej i więcej.

Materiały:

0
hauleth napisał(a):

W czasach Dockera i k8s każdy głupi język programowania to ma, nawet nie tylko to

Tak, ale nie do końca. Docker oferuje Ci to na poziomie systemu operacyjnego, tak samo jak każdy inny init-system (bogowie jak ja nie cierpię tej współczesnej "dokeryzacji" wszystkiego co się da). Ok, położy Ci się cała aplikacja i po chwili w stanie, ale z racji, że to proces systemowy to to z reguły trochę trwa.

Mam LB i w dockerze czy k8s nie wspominając i ja nie mam żadnego disruption time, chyba że padną wszystkie pody/nody na raz. To też mi umożliwia deployment innych wersji na część infrastruktury, bez obawy, że ona padnie (jak źle działa, to jest odcinana od ruchu). Nie widzę co mi tu ekstra Erlang zaoferuje.

Chyba, że odpalasz nowy kontener Dockera dla każdego osobnego połączenia HTTP. Erlang oferuje bardziej podejście jak FaaS niż sam Docker.

Ja to mogę obsługiwać na poziomie LB lub konkretnej apki, która dostaje ruch. I bedzie mi to działać na wszystkich stackach bez problemu, właśnie dzięki rozwiązaniom typu docker/k8s. Nawet zakładając hipotetycznie, że Erlang ma to najdoskonalej rozwiązane pod jakimiś tam względami (w co wątpie, bo po coś te k8s powstały gdy on już był), to i tak świat nie składa się wyłącznie z programów w Erlangu i nawet mniejsze projekty mają kilka stacków, które muszą ze sobą współgrać. Jak jesteś w stanie w swoich projektach tylko Erlanga używać to good for you.

bo można określić zaawansowane zależności

Nie do takiego stopnia jak w Erlangu.

Ja nie wiem jak to jest w Erlangu. W dockerze/k8s jest to IMHO zaawansowane, bo można określić co zależy od czego (co ma wcześniej wstać/podnieść się, a co później, pod jakimi warunkami, itp), możesz dopiąć swoje checki, które będą decydować czy serwis żyje/działa poprawnie itp. Zresztą, podobnie jest w przypadku sławetnego systemd - tu się nie da rocket science już wymyślić na tym polu. Podejrzewam, że Erlang nie wymysla też (w końcu to vmka), może coś dodaje do swojego stacka, ale cel jest ten sam: uniknąć przestoju aplikacji

W Dokerze zależności masz w większości "płaskie". W Erlangu mogę zbudować drzewo zależności, gdzie na różnych poziomach mam różne zachowania w przypadku błędu.
Na ten przykład mam "głównego" supervisora, który działa na zasadzie "rest for one", czyli jeśli jedno dziecko umrze to wszystkie "młodsze" niż on są ubijane i restartowane. W związku z czym np. > jak DB leży to zrestartuje się również serwer HTTP czy inne elementy.

Lepiej by do mnie przemówił usecase co mi to da, na poziomie architektury np. Przed czym mnie to ochroni, a przed czym nie (i dlaczego akurat Erlang mi na to pozwoli, a język X już nie)

Natomiast samo połączenie z DB działa na zasadzie "one for one" gdzie jeśli jedno połączenie padło, to zostanie zastąpione nowym, bez ingerencji w pozostałe. Daje nam to ogromne możliwości > nie tylko na kontrolę procesów, ale również na ograniczanie rażenia błędów.

Przecież to rozwiązuje zwykła pula połączeń do bazy, w Pythonie i Go to jest "w standardzie" w libkach, podejrzewam, że w innych językach tym bardziej. Czyli mogę na poziomie np. PgSQL skillować połączenie co robi coś na jednej tabeli i nie martwić się, że pobieranie danych w innych wątku mi padnie.

Już pomijam tutaj sprawy jak monitory czy rozproszony supervising przez co programista może "transparentnie" traktować cały klaster maszyn Erlanga jak jedną maszynę (bo komunikacja opiera > się na wysyłaniu wiadomości).

Zatem wynoszenie tej "codziennej" zalety uważam za coś dziwnego, albo ja czegoś tu nie rozumiem z tym Erlangiem (?)

Parę rzeczy:

  • Erlang jest starszy od Dockera, Erlang jest starszy od Linuksa (o 5 lat). Przez ten czas naprawdę wiele problemów już zauważono i rozwiązano
  • Erlang de facto to soft-real-time preemptive multiprocess OS, w którym bardzo do serca wzięto sobie stabilność systemu (dziewięć dziewiątek stabilności - https://www.rabbitmq.com/resources/armstrong.pdf strona 27)
  • Procesy Erlanga są zdecydowanie "tańsze" niż kontenery Dockera, można też je odpalać szybciej i więcej.

Ja nie umniejszam Erlangowi, chcę jednak powiedzieć, że obecnie jest już 2020, a nie 198* rok. Obecne techstacki mają wiele problemów już rozwiązanych od tego czasu. Erlang na pewno ma swoje zalety, ale te, które wymieniasz, to dziś się trochę zdewaluowały.

1 użytkowników online, w tym zalogowanych: 0, gości: 1