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?
c#
functional-programming
pure-function
Rock Anthony Johnson
źródło
źródło
Create
jest również nieczysty, ponieważ ją wywołuje.Create
funkcja 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.Odpowiedzi:
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ą,
return
rzeczywisty obiekt, który zawiera oczekiwaną wartość z funkcji oraz wartość wskazującą możliwy warunek błędu, taki jakMaybe
obiekt lub obiekt Jednostka pracy.źródło
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:
Jeśli
func
jest czysty, optymalizator może zdecydować, żefunc(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ł
Error
klasę taką jak Java (i wiele innych języków), sugerowałbym, aby rzucićError
zamiastException
. Wskazuje, że użytkownik funkcji zrobił coś złego (przekazał funkcję rzucającą) i takie rzeczy są dozwolone w czystych funkcjach. Ale C # nie maError
klasy, 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ą.
źródło
(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ę.func
, blok, który podałem, mógłby zostać zoptymalizowany, ponieważ zamiast odwijania stosu zostałby zakodowany wyjątekignoreThis
, 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ć.func(arg)
zwróci krotkę(<junk>, TheException())
, a więcignoreThis
będzie zawierała krotkę(<junk>, TheException())
, ale ponieważ nigdy nie używamyignoreThis
wyjątku, nie będzie to miało żadnego wpływu na nic.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:
Jak powiedział powyżej Bart van Ingen Schenau:
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 :
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:
źródło
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ć).
źródło
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.
źródło
f
ig
oba są funkcjami czystymi, tof(x) + g(x)
powinien mieć ten sam wynik niezależnie od kolejności, którą oceniaszf(x)
ig(x)
. Ale jeślif(x)
rzucaF
ig(x)
rzucaG
, wówczas wyjątek wyrzucony z tego wyrażenia byłby,F
gdybyf(x)
był oceniany jako pierwszy, aG
jeśli byłbyg(x)
oceniany jako pierwszy - nie tak powinny się zachowywać czyste funkcje! Gdy wyjątek jest zakodowany w typie zwracanym, wynik byłby podobny doError(F) + Error(G)
niezależnego od kolejności oceny.