Widzę korzyści wynikające ze stosowania obiektów mutowalnych i niezmiennych, takich jak obiekty niezmienne, które zabierają wiele trudnych do rozwiązania problemów w programowaniu wielowątkowym ze względu na stan współdzielenia i zapisu. Wręcz przeciwnie, zmienne obiekty pomagają radzić sobie z tożsamością obiektu, zamiast tworzyć nowe kopie za każdym razem, a tym samym poprawiają wydajność i zużycie pamięci, szczególnie dla większych obiektów.
Jedną rzeczą, którą próbuję zrozumieć, jest to, co może pójść nie tak, mając zmienne obiekty w kontekście programowania funkcjonalnego. Jednym z punktów, które mi powiedziano, jest to, że wynik wywoływania funkcji w innej kolejności nie jest deterministyczny.
Szukam prawdziwego konkretnego przykładu, w którym jest bardzo oczywiste, co może pójść nie tak przy użyciu zmiennego obiektu w programowaniu funkcji. Zasadniczo, jeśli jest zły, jest zły niezależnie od OO lub paradygmatu programowania funkcjonalnego, prawda?
Uważam, że poniżej moje własne oświadczenie odpowiada na to pytanie. Ale wciąż potrzebuję jakiegoś przykładu, aby poczuć to bardziej naturalnie.
OO pomaga zarządzać zależnościami i pisać łatwiejszy w utrzymaniu program za pomocą narzędzi takich jak enkapsulacja, polimorfizm itp.
Programowanie funkcjonalne ma również ten sam motyw promowania łatwego do utrzymania kodu, ale za pomocą stylu, który eliminuje potrzebę korzystania z narzędzi i technik OO - jednym z nich, moim zdaniem, jest minimalizacja efektów ubocznych, czystej funkcji itp.
Odpowiedzi:
Myślę, że znaczenie najlepiej można wykazać w porównaniu z podejściem otwartym
np. powiedzmy, że mamy obiekt
W paradygmacie OO metoda jest dołączona do danych i sensowne jest zmutowanie tych danych metodą.
W paradygmacie funkcjonalnym definiujemy wynik w kategoriach funkcji. zakupione zamówienie JEST wynikiem zastosowania funkcji zakupu zastosowanej do zamówienia. Oznacza to kilka rzeczy, których musimy być pewni
Czy spodziewałbyś się zamówienia. Stan == „Kupiony”?
Oznacza to również, że nasze funkcje są idempotentne. to znaczy. dwukrotne ich uruchomienie powinno za każdym razem dawać ten sam wynik.
Gdyby zamówienie zostało zmienione przez funkcję zakupu, zakupione Zamówienie2 nie powiedzie się.
Poprzez zdefiniowanie rzeczy jako wyników funkcji pozwala nam wykorzystać te wyniki bez faktycznego ich obliczania. Co z punktu widzenia programowania jest odroczeniem wykonania.
Może się to przydać samo w sobie, ale kiedy nie jesteśmy pewni, kiedy funkcja faktycznie się wydarzy, i nic nam nie jest, możemy wykorzystać przetwarzanie równoległe znacznie bardziej niż w paradygmacie OO.
Wiemy, że uruchomienie funkcji nie wpłynie na wyniki innej funkcji; więc możemy zostawić komputer, aby wykonał je w dowolnej kolejności, używając dowolnej liczby wątków.
Jeśli funkcja mutuje swoje wejście, musimy być bardziej ostrożni w takich sprawach.
źródło
Order Purchase() { return new Order(Status = "Purchased") }
tak, aby status był tylko do odczytu. ? Znowu dlaczego ta praktyka jest bardziej odpowiednia w kontekście paradygmatu programowania funkcji? Korzyści, o których wspomniałeś, można również zobaczyć w programowaniu OO, prawda?Kluczem do zrozumienia, dlaczego niezmienne obiekty są korzystne, nie jest tak naprawdę próba znalezienia konkretnych przykładów w kodzie funkcjonalnym. Ponieważ większość kodu funkcjonalnego jest napisana przy użyciu języków funkcjonalnych, a większość języków funkcjonalnych jest domyślnie niezmienna, sama natura paradygmatu została zaprojektowana w taki sposób, aby uniknąć tego, czego się szuka.
Kluczową kwestią jest pytanie, jaka jest korzyść z niezmienności? Odpowiedź brzmi: unika złożoności. Powiedzmy, że mamy dwie zmienne
x
iy
. Oba zaczynają się od wartości1
.y
chociaż podwaja się co 13 sekund. Jaka będzie wartość każdego z nich za 20 dni?x
będzie1
. To łatwe. Wypracowaniey
tego wymagałoby wysiłku, ponieważ jest znacznie bardziej złożone. O której porze dnia za 20 dni? Czy muszę brać pod uwagę czas letni? Złożonośćy
kontrax
jest o wiele więcej.I dzieje się tak również w prawdziwym kodzie. Za każdym razem, gdy dodajesz do miksu wartość mutującą, staje się to kolejną złożoną wartością, którą możesz trzymać i obliczyć w głowie lub na papierze, gdy próbujesz pisać, czytać lub debugować kod. Im większa złożoność, tym większa szansa, że popełnisz błąd i wprowadzisz błąd. Kod jest trudny do napisania; ciężkie do przeczytania; trudne do debugowania: trudno jest poprawnie uzyskać kod.
Zmienność nie jest jednak zła . Program z zerową zmiennością może nie dać rezultatu, co jest dość bezużyteczne. Nawet jeśli zmienność polega na zapisaniu wyniku na ekranie, dysku lub czymkolwiek, musi tam być. Złe jest niepotrzebna złożoność. Jednym z najprostszych sposobów zmniejszenia złożoności jest domyślnie uczynienie rzeczy niezmiennymi i modyfikowanie ich tylko w razie potrzeby, ze względu na wydajność lub ze względów funkcjonalnych.
źródło
y
musi mutować; to wymóg. Czasami musimy mieć złożony kod, aby spełnić złożone wymagania. Chodzi mi o to, że należy unikać niepotrzebnej złożoności. Mutowanie wartości jest z natury bardziej złożone niż ustalone, więc - aby uniknąć niepotrzebnej złożoności - mutuj wartości tylko wtedy, gdy musisz.Te same rzeczy, które mogą pójść nie tak w programowaniu niefunkcjonalnym: możesz uzyskać niepożądane, nieoczekiwane skutki uboczne , co jest dobrze znaną przyczyną błędów od czasu wynalezienia języków programowania z zakresem.
IMHO jedyną prawdziwą różnicą w tym między programowaniem funkcjonalnym a niefunkcjonalnym jest to, że w kodzie niefunkcjonalnym zwykle można spodziewać się efektów ubocznych, w programowaniu funkcjonalnym nie.
Pewnie - niepożądane efekty uboczne są kategorią błędów, niezależnie od paradygmatu. Odwrotna jest również prawda - celowo zastosowane efekty uboczne mogą pomóc poradzić sobie z problemami z wydajnością i są zwykle niezbędne w większości rzeczywistych programów, jeśli chodzi o operacje wejścia / wyjścia i systemy zewnętrzne - także niezależnie od paradygmatu.
źródło
Właśnie odpowiedziałem na pytanie StackOverflow, które dość dobrze ilustruje twoje pytanie. Główny problem ze zmiennymi strukturami danych polega na tym, że ich tożsamość jest ważna tylko w jednym momencie, więc ludzie starają się wcisnąć jak najwięcej w mały punkt kodu, w którym wiedzą, że tożsamość jest stała. W tym konkretnym przykładzie robi dużo logowania w pętli for:
Kiedy jesteś przyzwyczajony do niezmienności, nie musisz obawiać się zmiany struktury danych, jeśli będziesz czekać zbyt długo, dzięki czemu możesz wykonywać zadania, które są logicznie oddzielone w czasie wolnym, w znacznie bardziej oddzielony sposób:
źródło
Zaletą korzystania z niezmiennych obiektów jest to, że jeśli ktoś otrzyma referencję do obiektu, który będzie miał określoną właściwość, gdy odbiorca to sprawdzi, i będzie musiał podać jakiś inny kod referencji do obiektu o tej samej właściwości, można po prostu przekazać wzdłuż odniesienia do obiektu bez względu na to, kto jeszcze mógł otrzymać odniesienie lub co mogą zrobić z tym przedmiotem [ponieważ nic innego nie może zrobić z tym przedmiotem], lub gdy odbiorca może zbadać obiekt [ponieważ wszystkie jego właściwości będą takie same, niezależnie od tego, kiedy zostaną zbadane].
Natomiast kod, który musi dać komuś odniesienie do obiektu zmiennego, który będzie miał określoną właściwość, gdy odbiorca to zbada (zakładając, że sam odbiornik go nie zmieni), albo musi wiedzieć, że nic innego niż odbiorca nigdy się nie zmieni tę właściwość, albo wiedzieć, kiedy odbiorca będzie miał do niej dostęp, i wiedz, że nic nie zmieni tej właściwości, dopóki odbiorca nie sprawdzi jej ostatni raz.
Myślę, że najbardziej pomocne jest, aby programowanie ogólnie (nie tylko programowanie funkcjonalne) traktowało niezmienne obiekty jako trzy kategorie:
Obiekty, które nie mogą pozwolić, aby cokolwiek je zmieniło, nawet z referencją. Takie obiekty i odniesienia do nich zachowują się jak wartości i można je dowolnie udostępniać.
Przedmioty, które pozwalają się być zmieniane za pomocą kodu, który ma odniesień do nich, ale których odnośniki nigdy nie będzie narażony na jakiegokolwiek kodu, które faktycznie ich zmiany. Te obiekty zawierają wartości, ale można je udostępniać tylko kodowi, któremu można zaufać, że nie zmieni ich ani nie narazi na działanie kodu, który mógłby to zrobić.
Obiekty, które zostaną zmienione. Obiekty te najlepiej postrzegać jako kontenery , a odniesienia do nich jako identyfikatory .
Przydatnym wzorcem jest często utworzenie obiektu przez kontener, wypełnienie go za pomocą kodu, któremu można zaufać, aby nie zachowywał później referencji, a następnie posiadanie jedynych referencji, które kiedykolwiek będą istnieć gdziekolwiek we wszechświecie, w kodzie, który nigdy nie zmodyfikuje obiekt po wypełnieniu. Chociaż kontener może być typu zmiennego, można go argumentować (*) tak, jakby był niezmienny, ponieważ w rzeczywistości nic go nie zmutuje. Jeśli wszystkie odniesienia do kontenera są przechowywane w niezmiennych typach opakowań, które nigdy nie zmienią jego zawartości, takie opakowania mogą być bezpiecznie przekazywane, tak jakby dane w nich były przechowywane w niezmiennych obiektach, ponieważ odniesienia do opakowań mogą być swobodnie udostępniane i sprawdzane w w dowolnym momencie.
(*) W kodzie wielowątkowym może być konieczne użycie „barier pamięci”, aby upewnić się, że zanim dowolny wątek będzie mógł zobaczyć odniesienie do opakowania, skutki wszystkich działań na kontenerze będą widoczne dla tego wątku, ale to szczególny przypadek wspomniany tutaj tylko dla kompletności.
źródło
Jak już wspomniano, problem ze stanem zmiennym jest w zasadzie podklasą większego problemu skutków ubocznych , w którym typ zwracany przez funkcję nie opisuje dokładnie, co tak naprawdę robi funkcja, ponieważ w tym przypadku powoduje ona również mutację stanu. Problem ten został rozwiązany przez niektóre nowe języki badawcze, takie jak F * ( http://www.fstar-lang.org/tutorial/ ). Ten język tworzy system efektów podobny do systemu typów, w którym funkcja nie tylko statycznie deklaruje swój typ, ale także efekty. W ten sposób wywołujący funkcję zdają sobie sprawę, że mutacja stanu może wystąpić podczas wywoływania funkcji i że efekt jest propagowany do jej wywołujących.
źródło