Czy łapanie / rzucanie wyjątków sprawia, że ​​metoda, która w innym przypadku jest czysta, jest nieczysta?

27

Poniższe przykłady kodu stanowią kontekst mojego pytania.

Klasa pokoju jest inicjowana przez delegata. W pierwszej implementacji klasy Room nie ma strażników przed delegatami, którzy zgłaszają wyjątki. Takie wyjątki zostaną przeniesione do właściwości North, gdzie delegowany jest oceniany (uwaga: metoda Main () pokazuje, jak instancja Room jest używana w kodzie klienta):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Ponieważ wolałbym raczej nie tworzyć obiektu, niż czytając właściwość North, zmieniam konstruktor na prywatny i wprowadzam statyczną metodę fabryczną o nazwie Create (). Ta metoda przechwytuje wyjątek zgłoszony przez delegata i zgłasza wyjątek opakowania, mając znaczący komunikat o wyjątku:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Czy blok try-catch sprawia, że ​​metoda Create () jest zanieczyszczona?

Rock Anthony Johnson
źródło
1
Jakie są zalety funkcji tworzenia pokoju; jeśli używasz delegata, dlaczego dzwonisz, zanim klient woła „Północ”?
SH-
1
@SH Jeśli delegat zgłasza wyjątek, chcę się dowiedzieć po utworzeniu obiektu Room. Nie chcę, aby klient znalazł wyjątek po użyciu, ale po utworzeniu. Metoda Create to idealne miejsce do ujawnienia złych delegatów.
Rock Anthony Johnson
3
@RockAnthonyJohnson, Tak. Co robisz, wykonując delegata, możesz pracować tylko za pierwszym razem lub zwrócić inny pokój przy drugim połączeniu? Zadzwonienie do delegata można rozważyć / spowodować sam efekt uboczny?
SH-
4
Funkcja, która wywołuje funkcję nieczystą, jest funkcją nieczystą, więc jeśli delegat jest nieczysty, Createjest również nieczysty, ponieważ ją wywołuje.
Idan Arye
2
Twoja Createfunkcja nie chroni Cię przed uzyskaniem wyjątku podczas uzyskiwania nieruchomości. Jeśli twój delegat rzuca, w prawdziwym życiu jest bardzo prawdopodobne, że zostanie rzucony tylko pod pewnymi warunkami. Istnieje prawdopodobieństwo, że warunki do rzucania nie są obecne podczas budowy, ale występują przy zdobyciu nieruchomości.
Bart van Ingen Schenau

Odpowiedzi:

26

Tak. To faktycznie jest nieczysta funkcja. Powoduje to efekt uboczny: wykonywanie programu trwa w innym miejscu niż miejsce, do którego funkcja ma wrócić.

Aby uczynić ją czystą funkcją, returnrzeczywisty obiekt, który zawiera oczekiwaną wartość z funkcji oraz wartość wskazującą możliwy warunek błędu, taki jak Maybeobiekt lub obiekt Jednostka pracy.

Robert Harvey
źródło
16
Pamiętaj, że „czysty” to tylko definicja. Jeśli potrzebujesz funkcji, która rzuca, ale pod każdym innym względem jest referencyjnie przezroczysta, to właśnie piszesz.
Robert Harvey
1
W haskell co najmniej dowolna funkcja może rzucać, ale musisz złapać się IO, aby złapać, czysta funkcja, którą czasami rzuca, zwykle bywa nazywana częściową
jk.
4
Miałem objawienie z powodu twojego komentarza. Choć intelektualnie intryguje mnie czystość, w praktyce potrzebuję determinizmu i pełnej szacunku przejrzystości. Jeśli osiągnę czystość, to tylko dodatkowy bonus. Dziękuję Ci. Do Twojej wiadomości doprowadziło mnie to, że IntelliTest firmy Microsoft działa tylko przeciwko deterministycznemu kodowi.
Rock Anthony Johnson
4
@RockAnthonyJohnson: Cóż, cały kod powinien być deterministyczny, a przynajmniej tak deterministyczny, jak to tylko możliwe. Przejrzystość referencyjna jest tylko jednym ze sposobów na osiągnięcie tego determinizmu. Niektóre techniki, takie jak wątki, mają tendencję do działania przeciwko temu determinizmowi, dlatego istnieją inne techniki, takie jak Zadania, które poprawiają niedeterministyczne tendencje modelu wątkowania. Rzeczy takie jak generatory liczb losowych i symulatory Monte-Carlo są również deterministyczne, ale w inny sposób, w oparciu o prawdopodobieństwa.
Robert Harvey
3
@zzzzBov: Nie uważam faktycznej, ścisłej definicji „czystego” za tak ważne. Zobacz drugi komentarz powyżej.
Robert Harvey
12

Cóż, tak ... i nie.

Czysta funkcja musi mieć przezroczystość referencyjną - to znaczy powinieneś być w stanie zastąpić każde możliwe wywołanie funkcji czystej zwracaną wartością, bez zmiany zachowania programu. * Twoja funkcja zawsze rzuca określone argumenty, więc nie ma wartości zwracanej, która zastąpiłaby wywołanie funkcji, więc zamiast tego zignorujmy to. Rozważ ten kod:

{
    var ignoreThis = func(arg);
}

Jeśli funcjest czysty, optymalizator może zdecydować, że func(arg)można go zastąpić jego wynikiem. Nie wie jeszcze, jaki jest wynik, ale może stwierdzić, że nie jest on używany - więc może po prostu wydedukować to stwierdzenie bezskuteczne i usunąć je.

Ale jeśli func(arg)zdarzy się rzucić, to stwierdzenie coś robi - rzuca wyjątek! Optymalizator nie może go więc usunąć - nie ma znaczenia, czy funkcja zostanie wywołana, czy nie.

Ale...

W praktyce ma to bardzo małe znaczenie. Wyjątek - przynajmniej w języku C # - jest czymś wyjątkowym. Nie powinieneś używać go jako części regularnego przepływu kontroli - powinieneś spróbować go złapać, a jeśli coś złapiesz, obsłuż błąd, aby albo cofnąć to, co robiłeś, albo w jakiś sposób nadal to osiągnąć. Jeśli twój program nie działa poprawnie, ponieważ kod, który się nie powiódł, został zoptymalizowany, używasz wyjątków nieprawidłowo (chyba że jest to kod testowy, a kiedy budujesz dla testów, wyjątki nie powinny być optymalizowane).

Biorąc to pod uwagę ...

Nie rzucaj wyjątków od czystych funkcji z zamiarem ich złapania - istnieje dobry powód, dla którego języki funkcjonalne wolą używać monad zamiast wyjątków odwijania stosu.

Gdyby C # miał Errorklasę taką jak Java (i wiele innych języków), sugerowałbym, aby rzucić Errorzamiast Exception. Wskazuje, że użytkownik funkcji zrobił coś złego (przekazał funkcję rzucającą) i takie rzeczy są dozwolone w czystych funkcjach. Ale C # nie ma Errorklasy, a wyjątki błędów użytkowania wydają się wynikać Exception. Sugeruję, aby rzucić ArgumentException, wyjaśniając, że funkcja została wywołana ze złym argumentem.


* Mówiąc obliczeniowo. Funkcja Fibonacciego zaimplementowana przy użyciu naiwnej rekurencji zajmie dużo czasu dla dużych liczb i może wyczerpać zasoby maszyny, ale te, ponieważ z nieograniczonym czasem i pamięcią funkcja zawsze zwróci tę samą wartość i nie będzie mieć skutków ubocznych (innych niż przydzielanie pamięć i zmienianie tej pamięci) - nadal jest uważana za czystą.

Idan Arye
źródło
1
+1 Za sugerowane użycie ArgumentException i za zalecenie, aby nie „odrzucać wyjątków od czystych funkcji z zamiarem ich złapania”.
Rock Anthony Johnson
Twój pierwszy argument (aż do „Ale ...”) wydaje mi się niesłuszny. Wyjątek jest częścią umowy funkcji. Możesz, koncepcyjnie, przepisać dowolną funkcję jako (value, exception) = func(arg)i usunąć możliwość zgłoszenia wyjątku (w niektórych językach i dla niektórych typów wyjątków faktycznie musisz wymienić go z definicją, tak samo jak argumenty lub zwracana wartość). Teraz byłoby tak samo czyste jak poprzednio (pod warunkiem, że zawsze zwraca aka, zgłasza wyjątek z tym samym argumentem). Zgłaszanie wyjątku nie jest efektem ubocznym, jeśli zastosujesz tę interpretację.
AnoE
@AnoE Gdybyś tak napisał func, blok, który podałem, mógłby zostać zoptymalizowany, ponieważ zamiast odwijania stosu zostałby zakodowany wyjątek ignoreThis, który ignorujemy. W przeciwieństwie do wyjątków, które rozwijają stos, wyjątek zakodowany w typie zwracanym nie narusza czystości - i dlatego wiele języków funkcjonalnych woli je stosować.
Idan Arye
Dokładnie ... nie zignorowałbyś wyjątku dla tej wyobrażonej transformacji. Myślę, że to trochę kwestia semantyki / definicji, chciałem tylko zauważyć, że istnienie lub występowanie wyjątków niekoniecznie unieważnia wszelkie pojęcia czystości.
AnoE
1
@AnoE ale kod mam writted byłoby zignorować wyjątek dla tej wyobrażonej transformacji. func(arg)zwróci krotkę (<junk>, TheException()), a więc ignoreThisbędzie zawierała krotkę (<junk>, TheException()), ale ponieważ nigdy nie używamy ignoreThiswyjątku, nie będzie to miało żadnego wpływu na nic.
Idan Arye
1

Jedną z kwestii jest to, że blok try-catch nie jest problemem. (Na podstawie moich komentarzy do powyższego pytania).

Głównym problemem jest to, że właściwość North to wywołanie We / Wy .

W tym momencie wykonywania kodu program musi sprawdzić we / wy dostarczone przez kod klienta. (Nie ma znaczenia, że ​​dane wejściowe mają postać delegata lub że dane wejściowe zostały już nominalnie przekazane).

Po utracie kontroli nad wejściem nie można upewnić się, że funkcja jest czysta. (Zwłaszcza jeśli funkcja może rzucać).


Nie jestem pewien, dlaczego nie chcesz sprawdzać połączenia w celu przeniesienia [Sprawdź] pokoju? Zgodnie z moim komentarzem do pytania:

Tak. Co robisz, gdy wykonujesz pełnomocnika, możesz pracować tylko za pierwszym razem lub zwrócić inny pokój przy drugim połączeniu? Zadzwonienie do delegata można rozważyć / spowodować sam efekt uboczny?

Jak powiedział powyżej Bart van Ingen Schenau:

Twoja funkcja Utwórz nie chroni Cię przed uzyskaniem wyjątku podczas uzyskiwania właściwości. Jeśli twój delegat rzuca, w prawdziwym życiu jest bardzo prawdopodobne, że zostanie rzucony tylko pod pewnymi warunkami. Istnieje prawdopodobieństwo, że warunki do rzucania nie są obecne podczas budowy, ale występują przy zdobyciu nieruchomości.

Ogólnie rzecz biorąc, każdy rodzaj opóźnionego ładowania domyślnie odracza błędy do tego momentu.


Sugerowałbym użycie metody Move [Check] Room. Umożliwiłoby to podzielenie nieczystych aspektów We / Wy w jednym miejscu.

Podobnie do odpowiedzi Roberta Harveya :

Aby uczynić ją czystą funkcją, zwróć rzeczywisty obiekt, który zawiera oczekiwaną wartość z funkcji oraz wartość wskazującą możliwy błąd, na przykład obiekt Może lub obiekt Jednostki Pracy.

Od autora kodu zależałoby określenie, jak obsłużyć (możliwy) wyjątek z danych wejściowych. Następnie metoda może zwrócić obiekt Room lub obiekt Null Room lub wykreślić wyjątek.

W tym momencie zależy to od:

  • Czy domena pokoju traktuje wyjątki pokoju jako Null lub coś gorszego.
  • Jak powiadomić kod klienta wywołujący północ w pokoju zerowym / wyjątkowym. (Bail / Status Variable / Global State / Return a Monad / Anything; Niektóre są bardziej czyste niż inne :)).
SH-
źródło
-1

Hmm ... Nie miałem racji. Myślę, że to, co próbujesz zrobić, to upewnić się, że inni ludzie dobrze wykonują swoją pracę, nie wiedząc, co robią.

Ponieważ funkcja jest przekazywana przez konsumenta, który może być napisany przez ciebie lub inną osobę.

Co się stanie, jeśli przekazana funkcja nie będzie działać w momencie tworzenia obiektu Room?

Na podstawie kodu nie byłem pewien, co robi Północ.

Powiedzmy, że jeśli funkcja polega na zarezerwowaniu pokoju i potrzebuje czasu i okresu rezerwacji, to dostaniesz wyjątek, jeśli nie będziesz mieć przygotowanych informacji.

Co jeśli jest to funkcja długotrwała? Nie będziesz chciał blokować programu podczas tworzenia obiektu Pokój, a jeśli chcesz utworzyć obiekt 1000 Pokój?

Gdybyś był osobą, która napisze konsumenta korzystającego z tej klasy, upewnisz się, że przekazana funkcja jest napisana poprawnie, prawda?

Jeśli jest inna osoba, która pisze konsumenta, może on nie znać „specjalnego” warunku utworzenia obiektu Room, co może spowodować wykonanie, i podrapałyby się w głowę, próbując dowiedzieć się, dlaczego.

Inną rzeczą jest to, że nie zawsze dobrze jest rzucać nowy wyjątek. Powodem jest to, że nie będzie zawierać informacji o oryginalnym wyjątku. Wiesz, że pojawia się błąd (przyjazny komunikat dla ogólnego wyjątku), ale nie wiedziałbyś, jaki jest błąd (odwołanie zerowe, indeks poza zakresem, dzielenie przez zero itp.). Ta informacja jest bardzo ważna, gdy musimy przeprowadzić dochodzenie.

Powinieneś obsłużyć wyjątek tylko wtedy, gdy wiesz, co i jak sobie z nim poradzić.

Myślę, że twój oryginalny kod jest wystarczająco dobry, jedyne, co możesz chcieć obsłużyć wyjątek na „Main” (lub dowolnej warstwie, która wiedziałaby, jak to obsłużyć).

użytkownik251630
źródło
1
Spójrz jeszcze raz na przykład drugiego kodu; Inicjuję nowy wyjątek od niższego wyjątku. Cały ślad stosu zostaje zachowany.
Rock Anthony Johnson
Ups Mój błąd.
user251630 27.10.16
-1

Nie zgodzę się z innymi odpowiedziami i powiem „ nie” . Wyjątki nie powodują, że poza tym czysta funkcja staje się nieczysta.

Każde użycie wyjątków można przepisać, aby użyć jawnych kontroli wyników błędów. Wyjątki można po prostu uznać za cukier syntaktyczny. Wygodny cukier składniowy nie zanieczyszcza czystego kodu.

JacquesB
źródło
1
Fakt, że możesz przepisać nieczystości, nie czyni funkcji czystą. Haskell jest dowodem na to, że użycie nieczystych funkcji można przepisać przy użyciu czystych funkcji - używa monad do kodowania każdego nieczystego zachowania w typach zwracanych czysto czystych funkcji. Istnienie monad IO nie oznacza, że ​​zwykłe IO jest czyste - dlaczego istnienie monad wyjątków ma sugerować, że regularne wyjątki są czyste?
Idan Arye
@IdanArye: Twierdzę, że w ogóle nie ma nieczystości. Przykładem przepisywania było tylko zademonstrowanie tego. Regularne we / wy jest nieczyste, ponieważ powoduje obserwowalne skutki uboczne lub może zwracać różne wartości przy tym samym wejściu z powodu niektórych danych zewnętrznych. Zgłoszenie wyjątku nie powoduje żadnej z tych rzeczy.
JacquesB
Kod wolny od skutków ubocznych nie wystarczy. Referencyjna przejrzystość jest również warunkiem czystości funkcji.
Idan Arye
1
Determinizm nie wystarczy dla przejrzystości referencyjnej - skutki uboczne mogą być również deterministyczne. Przezroczystość referencyjna oznacza, że ​​wyrażenie można zastąpić jego wynikami - bez względu na to, ile razy oceniasz referencyjne przezroczyste wyrażenia, w jakiej kolejności i czy w ogóle je oceniasz - wynik powinien być taki sam. Jeśli wyjątek zostanie rzucony deterministycznie, jesteśmy dobrzy z kryteriami „bez względu na to, ile razy”, ale nie z innymi. Kłóciłem się o kryteria „czy”, czy nie we własnej odpowiedzi (cd.)
Idan Arye,
1
(... cd.) i jeśli chodzi o „w jakiej kolejności” - jeśli fi goba są funkcjami czystymi, to f(x) + g(x)powinien mieć ten sam wynik niezależnie od kolejności, którą oceniasz f(x)i g(x). Ale jeśli f(x)rzuca Fi g(x)rzuca G, wówczas wyjątek wyrzucony z tego wyrażenia byłby, Fgdyby f(x)był oceniany jako pierwszy, a Gjeśli byłby g(x)oceniany jako pierwszy - nie tak powinny się zachowywać czyste funkcje! Gdy wyjątek jest zakodowany w typie zwracanym, wynik byłby podobny do Error(F) + Error(G)niezależnego od kolejności oceny.
Idan Arye