co może pójść nie tak w kontekście programowania funkcjonalnego, jeśli mój obiekt jest zmienny?

9

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.

rahulaga_dev
źródło
1
@Ruben powiedziałbym, że większość języków funkcjonalnych dopuszcza zmienne zmienne, ale ich użycie różni się, np. Zmienne zmienne mają inny typ
jk.
1
Myślę, że w swoim pierwszym akapicie mogłeś mieszać niezmienny i zmienny?
jk.
1
@ jk., na pewno tak. Edytowane, aby to poprawić.
David Arno,
6
@Ruben Programowanie funkcjonalne jest paradygmatem. Jako taki nie wymaga funkcjonalnego języka programowania. Niektóre języki fp, takie jak F #, mają tę funkcję .
Christophe
1
@Ruben nie specjalnie myślałem o Mvars w haskell hackage.haskell.org/package/base-4.9.1.0/docs/… różne języki mają różne rozwiązania oczywiście lub hackage.haskell.org/package/base-4.11.1.0 IORefs /docs/Data-IORef.html choć oczywiście użyłbyś obu z monad
jk.

Odpowiedzi:

7

Myślę, że znaczenie najlepiej można wykazać w porównaniu z podejściem otwartym

np. powiedzmy, że mamy obiekt

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

W paradygmacie OO metoda jest dołączona do danych i sensowne jest zmutowanie tych danych metodą.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

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

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

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.

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

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.

Ewan
źródło
dzięki !! bardzo pomocny. Nowa implementacja zakupu wyglądałaby 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?
rahulaga_dev
w OO można oczekiwać, że obiekt.Purchase () zmodyfikuje obiekt. Możesz sprawić, by był niezmienny, ale dlaczego nie przejść do pełnego paradygmatu funkcjonalnego
Ewan
Myślę, że problem muszę wizualizować, ponieważ jestem czystym deweloperem c #, który jest zorientowany obiektowo przez naturę. Więc to, co mówisz w języku obejmującym programowanie funkcjonalne, nie będzie wymagało, aby funkcja „Purchase ()” zwracała zakupione zamówienie, aby było dołączone do dowolnej klasy lub obiektu, prawda?
rahulaga_dev
3
możesz napisać funkcjonalne c # zmienić swój obiekt w strukturę, uczynić go niezmiennym i napisać Func <Zamów, zamów> Zakup
Ewan
12

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 xi y. Oba zaczynają się od wartości 1. ychociaż podwaja się co 13 sekund. Jaka będzie wartość każdego z nich za 20 dni? xbędzie 1. To łatwe. Wypracowanie ytego 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ść ykontra xjest 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.

David Arno
źródło
4
„Jednym z najprostszych sposobów zmniejszenia złożoności jest domyślnie uczynienie rzeczy niezmiennymi i modyfikowanie ich tylko w razie potrzeby”: Bardzo ładne i zwięzłe podsumowanie.
Giorgio
2
@DavidArno Złożoność, którą opisujesz, sprawia, że ​​kod jest trudny do uzasadnienia. Dotknąłeś tego również, mówiąc: „Kod jest trudny do napisania; trudny do odczytania; trudny do debugowania; ...”. Lubię niezmienne obiekty, ponieważ znacznie ułatwiają rozumowanie kodu, nie tylko przeze mnie, ale także obserwatorów, którzy patrzą na nie znając całego projektu.
demontować numer-5
1
@RahulAgarwal, „ Ale dlaczego ten problem staje się bardziej widoczny w kontekście programowania funkcjonalnego ”. Tak nie jest. Myślę, że może jestem zdezorientowany tym, o co pytasz, ponieważ problem jest znacznie mniej widoczny w FP, ponieważ FP zachęca do niezmienności, unikając w ten sposób problemu.
David Arno
1
@djechlin, „ Jak twój 13-sekundowy przykład może stać się łatwiejszy do analizy za pomocą niezmiennego kodu? ” Nie może: ymusi 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.
David Arno
3
Zmienność powoduje kryzys tożsamości. Twoja zmienna nie ma już jednej tożsamości. Zamiast tego jego tożsamość zależy teraz od czasu. Zatem symbolicznie zamiast jednego x mamy teraz rodzinę x_t. Każdy kod używający tej zmiennej będzie teraz musiał również martwić się czasem, powodując dodatkową złożoność wspomnianą w odpowiedzi.
Alex Vong
8

co może pójść nie tak w kontekście programowania funkcjonalnego

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.

Zasadniczo, jeśli jest zły, jest zły niezależnie od OO lub paradygmatu programowania funkcjonalnego, prawda?

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.

Doktor Brown
źródło
4

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:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

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:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
Karl Bielefeldt
źródło
3

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:

  1. 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ć.

  2. 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ć.

  3. 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.

supercat
źródło
dzięki za imponującą odpowiedź !! Myślę, że prawdopodobnie źródłem mojego zamieszania jest to, że pochodzę z języka c # i uczę się "pisania kodu stylu funkcjonalnego w języku c #", który ciągle mówi wszędzie, unikając obiektów zmiennych - ale myślę, że języki, które obejmują funkcjonalny paradygmat programowania promują (lub egzekwują - nie jestem pewien jeśli egzekwowanie jest poprawne w użyciu) niezmienność.
rahulaga_dev
@RahulAgarwal: Możliwe jest, że odwołania do obiektu zawierają wartość, na którą znaczenie nie ma wpływu istnienie innych odwołań do tego samego obiektu, mają tożsamość, która kojarzyłaby je z innymi odniesieniami do tego samego obiektu, lub żadną z nich. Jeśli zmieni się stan słowa rzeczywistego, wówczas wartość lub tożsamość obiektu powiązanego z tym stanem może być stała, ale nie jedno i drugie - trzeba będzie zmienić. 50 000 $ jest tym, co powinno zrobić.
supercat
1

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.

Aaron M. Eshbach
źródło