Może monada kontra wyjątki

Odpowiedzi:

57

Użycie Maybe(lub jego kuzyna, Eitherktóry działa zasadniczo w ten sam sposób, ale pozwala zwrócić dowolną wartość zamiast Nothing) służy nieco innym celowi niż wyjątki. W kategoriach Java przypomina to sprawdzony wyjątek, a nie wyjątek środowiska wykonawczego. Reprezentuje ona coś oczekiwanego które masz do czynienia, zamiast błędu nie spodziewał.

Tak więc funkcja indexOftaka zwróciłaby Maybewartość, ponieważ oczekujesz możliwości, że elementu nie ma na liście. Przypomina to powrót nullz funkcji, z wyjątkiem bezpiecznego typu, który zmusza cię do załatwienia nullsprawy. Eitherdziała w ten sam sposób, z tą różnicą, że możesz zwrócić informacje związane z przypadkiem błędu, więc jest to bardziej podobne do wyjątku niż Maybe.

Jakie są zalety podejścia Maybe/ Either? Po pierwsze, jest to obywatel pierwszej klasy tego języka. Porównajmy funkcję używającą Eitherdo zgłaszania wyjątku. W przypadku wyjątku jedynym realnym wyjściem jest try...catchstwierdzenie. W przypadku tej Eitherfunkcji można użyć istniejących kombinatorów, aby uczynić kontrolę przepływu bardziej przejrzystą. Oto kilka przykładów:

Po pierwsze, powiedzmy, że chcesz wypróbować kilka funkcji, które mogą powodować błędy w szeregu, dopóki nie otrzymasz takiej, która tego nie robi. Jeśli nie otrzymasz żadnego bez błędów, chcesz zwrócić specjalny komunikat o błędzie. Jest to w rzeczywistości bardzo przydatny wzór, ale korzystanie z niego byłoby okropnym bólem try...catch. Na szczęście, ponieważ Eitherjest to po prostu wartość normalna, możesz użyć istniejących funkcji, aby uczynić kod znacznie wyraźniejszym:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Innym przykładem jest posiadanie opcjonalnej funkcji. Załóżmy, że masz kilka funkcji do uruchomienia, w tym jedną, która próbuje zoptymalizować zapytanie. Jeśli to się nie powiedzie, chcesz, aby wszystko inne działało tak czy inaczej. Możesz napisać kod coś takiego:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Oba te przypadki są wyraźniejsze i krótsze niż użycie try..catch, a co ważniejsze, bardziej semantyczne. Używanie funkcji podobnej do <|>lub optionalsprawia, że ​​twoje intencje są znacznie bardziej przejrzyste niż używanie try...catchzawsze do obsługi wyjątków.

Pamiętaj również, że nie musisz zaśmiecać kodu liniami takimi jak if a == Nothing then Nothing else ...! Istotą leczenia Maybei Eitherjak monady jest, aby tego uniknąć. Możesz zakodować semantykę propagacji w funkcji wiązania, aby uzyskać bezpłatne sprawdzanie wartości null / error. Jedyne, co musisz jawnie sprawdzić, to czy chcesz zwrócić coś innego niż Nothingpodane Nothing, a nawet wtedy jest to łatwe: istnieje kilka standardowych funkcji bibliotecznych, które poprawią ten kod.

Wreszcie kolejną zaletą jest to, że typ Maybe/ Eitherjest po prostu prostszy. Nie ma potrzeby rozszerzania języka o dodatkowe słowa kluczowe lub struktury kontrolne - wszystko to tylko biblioteka. Ponieważ są to po prostu wartości normalne, upraszcza to system typów - w Javie musisz rozróżniać typy (np. Typ zwracany) i efekty (np. throwsInstrukcje), których nie używasz Maybe. Zachowują się również tak samo, jak każdy inny typ zdefiniowany przez użytkownika - nie ma potrzeby wprowadzania specjalnego kodu obsługi błędów do języka.

Kolejną wygraną jest to, że Maybe/ Eithersą funktorami i monadami, co oznacza, że ​​mogą skorzystać z istniejących funkcji kontrolujących monadę (których jest sporo) i ogólnie grać ładnie z innymi monadami.

To powiedziawszy, są pewne zastrzeżenia. Po pierwsze, ani Maybenie Eitherzastępuj niezaznaczonych wyjątków. Będziesz potrzebować innego sposobu radzenia sobie z takimi rzeczami, jak dzielenie przez 0, po prostu dlatego, że każdy pojedynczy podział zwróci Maybewartość.

Innym problemem jest zwracanie wielu rodzajów błędów (dotyczy to tylko Either). Z wyjątkami możesz rzucać różne typy wyjątków w tej samej funkcji. z Either, dostajesz tylko jeden typ. Można to obejść za pomocą podtypów lub narzędzia ADT zawierającego wszystkie różne typy błędów jako konstruktorów (to drugie podejście jest zwykle stosowane w Haskell).

Mimo wszystko wolę podejście Maybe/, Eitherponieważ uważam, że jest prostsze i bardziej elastyczne.

Tikhon Jelvis
źródło
W większości uzgodnione, ale nie sądzę, aby przykład „opcjonalny” miał sens - wydaje się, że można to zrobić równie dobrze z wyjątkami: void Optional (Action act) {try {act (); } catch (wyjątek) {}}
Errorsatz
11
  1. Wyjątek może zawierać więcej informacji o źródle problemu. OpenFile()może rzucać FileNotFoundlub NoPermissionlub TooManyDescriptorsitp Żaden nie prowadzi tej informacji.
  2. Wyjątków można używać w kontekstach, w których brakuje wartości zwracanych (np. W przypadku konstruktorów w językach, które je mają).
  3. Wyjątek pozwala bardzo łatwo wysyłać informacje do stosu, bez wielu if None return Noneinstrukcji typu.
  4. Obsługa wyjątków prawie zawsze ma większy wpływ na wydajność niż tylko zwracanie wartości.
  5. Co najważniejsze, wyjątek i monada Może mają różne cele - wyjątek jest używany do oznaczenia problemu, a być może nie.

    „Siostro, jeśli w pokoju 5 jest pacjent, czy możesz poprosić go, aby zaczekał?”

    • Może monada: „Doktorze, w pokoju 5 nie ma pacjenta”.
    • Wyjątek: „Doktorze, nie ma miejsca 5!”

    (zauważ „jeśli” - to znaczy, że lekarz spodziewa się monady Może)

Dąb
źródło
7
Nie zgadzam się z punktami 2 i 3. W szczególności 2 nie stanowi problemu w językach, które używają monad, ponieważ te języki mają konwencje, które nie generują kłopotów z koniecznością radzenia sobie z awarią podczas budowy. Jeśli chodzi o 3, języki z monadami mają odpowiednią składnię do obsługi monad w przejrzysty sposób, który nie wymaga jawnego sprawdzania (na przykład Nonewartości można po prostu propagować). Twój punkt 5 jest tylko rodzajem racji… pytanie brzmi: które sytuacje są jednoznacznie wyjątkowe? Jak się okazuje… nie wielu .
Konrad Rudolph
3
Jeśli chodzi o (2), możesz pisać bindw taki sposób, że testowanie pod kątem Nonenie powoduje jednak narzutu składniowego. Bardzo prosty przykład, C # po prostu odpowiednio przeciąża Nullableoperatorów. Nie jest Nonekonieczne sprawdzanie , nawet przy użyciu tego typu. Oczywiście kontrola jest nadal wykonywana (jest bezpieczna pod względem typu), ale za kulisami i nie zaśmieca kodu. To samo dotyczy w pewnym sensie twojego sprzeciwu wobec mojego sprzeciwu wobec (5), ale zgadzam się, że nie zawsze może mieć zastosowanie.
Konrad Rudolph
4
@Oak: Jeśli chodzi o (3), cały sens traktowania Maybejako monady polega na upowszechnieniu propagacji None. Oznacza to, że jeśli chcesz zwrócić Nonedane dane None, nie musisz pisać żadnego specjalnego kodu. Musisz tylko dopasować, jeśli chcesz zrobić coś wyjątkowego None. Nigdy nie potrzebujesz if None then Noneczegoś w rodzaju oświadczeń.
Tikhon Jelvis
5
@Oak: to prawda, ale tak naprawdę nie o to mi chodziło. Mówię, że nullsprawdzanie dokładnie tak (np. if Nothing then Nothing) Za darmo, ponieważ Maybejest monadą. Jest zakodowany w definicji bind ( >>=) dla Maybe.
Tikhon Jelvis
2
Również w odniesieniu do (1): możesz łatwo napisać monadę, która może przenosić informacje o błędach (np. Either), Która zachowuje się tak samo Maybe. Przełączanie między nimi jest w rzeczywistości dość proste, ponieważ Maybetak naprawdę jest to szczególny przypadek Either. (W Haskell można by pomyśleć Maybejako Either ().)
Tikhon Jelvis
6

„Może” nie zastępuje wyjątków. Wyjątki mają być stosowane w wyjątkowych przypadkach (na przykład: otwieranie połączenia z bazą danych, a serwera db nie ma, chociaż powinno być). „Może” służy do modelowania sytuacji, w której możesz mieć lub nie mieć prawidłową wartość; powiedzmy, że otrzymujesz wartość ze słownika dla klucza: może istnieć lub nie - nie ma nic „wyjątkowego” w żadnym z tych wyników.

Nemanja Trifunovic
źródło
2
Właściwie mam sporo kodu zawierającego słowniki, w których brakujący klucz oznacza, że ​​coś poszło bardzo źle w pewnym momencie, najprawdopodobniej błąd w moim kodzie lub w kodzie, który przekonwertował dane wejściowe na wszystko, co zostało użyte w tym momencie.
3
@delnan: Nie twierdzę, że brakująca wartość nigdy nie może być oznaką wyjątkowego stanu - po prostu nie musi tak być. Może naprawdę modeluje sytuację, w której zmienna może mieć, ale nie musi, prawidłową wartość - pomyśl typy zerowe w języku C #.
Nemanja Trifunovic
6

Popieram odpowiedź Tichona, ale myślę, że jest bardzo ważny praktyczny punkt, którego wszyscy brakuje:

  1. Mechanizmy obsługi wyjątków, przynajmniej w językach głównego nurtu, są ściśle powiązane z poszczególnymi wątkami.
  2. EitherMechanizm nie jest sprzężony z gwintem na wszystkich.

Tak więc w dzisiejszych czasach widzimy, że wiele rozwiązań programowania asynchronicznego przyjmuje wariant Eitherstylu obsługi błędów. Zastanów się nad obietnicami Javascript , jak szczegółowo opisano w dowolnym z tych linków:

Koncepcja obietnic pozwala pisać kod asynchroniczny w następujący sposób (wzięty z ostatniego linku):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Zasadniczo obietnica to obiekt, który:

  1. Reprezentuje wynik obliczeń asynchronicznych, które mogły, ale nie zostały jeszcze zakończone;
  2. Pozwala na połączenie kolejnych operacji do wykonania na jego wyniku, które zostaną uruchomione, gdy ten wynik będzie dostępny, a których wyniki z kolei będą dostępne jako obietnice;
  3. Umożliwia podłączenie modułu obsługi błędów, który zostanie wywołany w przypadku niepowodzenia obliczenia obietnicy. Jeśli nie ma procedury obsługi, błąd jest propagowany do późniejszych procedur obsługi w łańcuchu.

Zasadniczo, ponieważ natywna obsługa wyjątków języka nie działa, gdy obliczenia odbywają się w wielu wątkach, implementacja obietnic musi zapewniać mechanizm obsługi błędów, a te okazują się być monadami podobnymi do typów Maybe/ Eithertypów Haskella .

sacundim
źródło
Nie ma nic do zrobienia z wątkami. JavaScript w przeglądarce zawsze działa w jednym wątku, a nie w wielu wątkach. Ale nadal nie możesz użyć wyjątku, ponieważ nie wiesz, kiedy twoja funkcja zostanie wywołana w przyszłości. Asynchroniczny nie oznacza automatycznie zaangażowania wątków. To jest również powód, dla którego nie możesz pracować z wyjątkami. Możesz pobrać wyjątek tylko wtedy, gdy wywołasz funkcję i natychmiast zostanie ona wykonana. Ale głównym celem Asynchronous jest to, że działa w przyszłości, często gdy coś innego się skończy, a nie natychmiast. Dlatego nie można tam stosować wyjątków.
David Raab,
1

System typu Haskell będzie wymagał od użytkownika potwierdzenia możliwości a Nothing, podczas gdy języki programowania często nie wymagają wychwycenia wyjątku. Oznacza to, że w czasie kompilacji będziemy wiedzieć, że użytkownik sprawdził, czy nie wystąpił błąd.

chrisaycock
źródło
1
Istnieją sprawdzone wyjątki w Javie. I ludzie często źle ich traktowali.
Vladimir
1
@ DairT'arg ButJava wymaga sprawdzania błędów IO (na przykład), ale nie NPE. W Haskell jest odwrotnie: sprawdzany jest równoważnik zerowy (Być może), ale błędy We / Wy po prostu zgłaszają (niezaznaczone) wyjątki. Nie jestem pewien, ile to stanowi różnicę, ale nie słyszę, by Haskellers narzekał na Być może i myślę, że wiele osób chciałoby sprawdzić NPE.
1
@delnan Ludzie chcą sprawdzania NPE? Muszą być szaleni. I mam na myśli to. Sprawdzanie NPE ma sens tylko wtedy, gdy masz sposób na uniknięcie go w pierwszej kolejności (poprzez niepoprawne odwołania, których Java nie ma).
Konrad Rudolph
5
@KonradRudolph Tak, ludzie prawdopodobnie nie chcą, aby to sprawdzano w tym sensie, że będą musieli dodać throws NPEdo każdej podpisu i catch(...) {throw ...} do każdej treści metody. Ale wierzę, że istnieje rynek dla sprawdzanych w tym samym sensie, co z Może: zerowanie jest opcjonalne i śledzone w systemie typów.
1
@delnan Ah, więc zgadzam się.
Konrad Rudolph
0

Być może monada jest w zasadzie taka sama, jak w przypadku głównego języka stosowanie sprawdzania „null oznacza błąd” (z tym że wymaga sprawdzenia wartości null) i ma w dużej mierze te same zalety i wady.

Telastyn
źródło
8
Cóż, nie ma tych samych wad, ponieważ można go sprawdzić statycznie, jeśli jest poprawnie używany. Nie ma ekwiwalentu wyjątku zerowego wskaźnika, gdy używasz być może monad (znowu, zakładając, że są one używane poprawnie).
Konrad Rudolph
W rzeczy samej. Jak według ciebie należy to wyjaśnić?
Telastyn
@Konrad To dlatego, że aby użyć wartości (być może) w Może, musisz sprawdzić Brak (chociaż oczywiście istnieje możliwość zautomatyzowania dużej części tego sprawdzania). Inne języki trzymają tego rodzaju podręczniki (głównie z powodów filozoficznych, moim zdaniem).
Donal Fellows
2
@Donal Tak, ale należy pamiętać, że większość języków, które implementują monady, zapewnia odpowiedni cukier składniowy, dzięki czemu to sprawdzenie może często zostać całkowicie ukryte. Na przykład możesz po prostu dodać dwie Maybeliczby, pisząc a + b bez konieczności sprawdzania None, a wynik jest ponownie wartością opcjonalną.
Konrad Rudolph
2
Porównanie z typami zerowalnymi jest prawdziwe dla tego Maybe typu , ale użycie Maybejako monady dodaje cukier składniowy, który pozwala na bardziej eleganckie wyrażanie logiki zerowej.
tdammers
0

Obsługa wyjątków może być prawdziwym problemem dla faktoringu i testowania. Wiem, że Python zapewnia ładną składnię „z”, która pozwala wychwytywać wyjątki bez sztywnego bloku „try ... catch”. Ale na przykład w Javie wypróbuj bloki catch, które są duże, pełne, pełne lub bardzo szczegółowe i trudne do rozbicia. Ponadto Java dodaje cały szum wokół zaznaczonych i niesprawdzonych wyjątków.

Jeśli zamiast tego twoja monada wyłapuje wyjątki i traktuje je jako właściwość przestrzeni monadycznej (zamiast jakiejś anomalii przetwarzania), możesz dowolnie mieszać i dopasowywać funkcje powiązane z tym obszarem, niezależnie od tego, co rzucą lub złapią.

Jeśli, jeszcze lepiej, twoja monada zapobiega warunkom, w których mogłyby się zdarzyć wyjątki (np. Wypchnięcie czeku zerowego do Może), to jeszcze lepiej. jeśli ... to jest dużo, dużo łatwiej wziąć pod uwagę i przetestować niż spróbować ... złapać.

Z tego, co widziałem, Go przyjmuje podobne podejście, określając, że każda funkcja zwraca (odpowiedź, błąd). To trochę tak samo, jak „podniesienie” funkcji do przestrzeni monady, w której typ odpowiedzi na rdzeń jest ozdobiony wskazaniem błędu i skutecznie rzuca na bok wyjątki.

Obrabować
źródło