Zalety programowania bezstanowego?

133

Niedawno uczyłem się programowania funkcjonalnego (szczególnie Haskell, ale przeszedłem też przez samouczki na temat Lisp i Erlang). Chociaż uważam te koncepcje za bardzo pouczające, nadal nie widzę praktycznej strony koncepcji „bez skutków ubocznych”. Jakie są praktyczne zalety tego rozwiązania? Próbuję myśleć funkcjonalnie, ale są sytuacje, które wydają się zbyt skomplikowane bez możliwości zapisania stanu w łatwy sposób (nie uważam monad Haskella za „łatwe”).

Czy warto kontynuować dogłębną naukę języka Haskell (lub innego języka czysto funkcjonalnego)? Czy programowanie funkcjonalne lub bezstanowe jest faktycznie bardziej produktywne niż proceduralne? Czy jest prawdopodobne, że będę dalej używać Haskell lub innego języka funkcjonalnego później, czy też powinienem się go uczyć tylko dla zrozumienia?

Mniej zależy mi na wydajności niż na produktywności. Więc głównie pytam, czy będę bardziej produktywny w języku funkcjonalnym niż w języku proceduralnym / obiektowym / czymkolwiek.

Sasha Chedygov
źródło

Odpowiedzi:

169

Przeczytaj programowanie funkcjonalne w pigułce .

Programowanie bezstanowe ma wiele zalet, między innymi to dramatycznie wielowątkowy i współbieżny kod. Mówiąc wprost, stan zmienny jest wrogiem kodu wielowątkowego. Jeśli wartości są domyślnie niezmienne, programiści nie muszą się martwić, że jeden wątek zmieni wartość stanu współdzielonego między dwoma wątkami, więc eliminuje to całą klasę wielowątkowości błędów związanych z warunkami wyścigu. Ponieważ nie ma warunków wyścigu, nie ma też powodu, aby używać blokad, więc niezmienność eliminuje kolejną całą klasę błędów związanych z zakleszczeniami.

To jest główny powód, dla którego programowanie funkcjonalne ma znaczenie i prawdopodobnie najlepszy do wskoczenia na pociąg programowania funkcjonalnego. Istnieje również wiele innych korzyści, w tym uproszczone debugowanie (tj. Funkcje są czyste i nie zmieniają stanu w innych częściach aplikacji), bardziej zwięzły i wyrazisty kod, mniej standardowy kod w porównaniu z językami silnie zależnymi od wzorców projektowych oraz kompilator może bardziej agresywnie optymalizować kod.

Julia
źródło
5
Popieram to! Wierzę, że programowanie funkcjonalne będzie w przyszłości wykorzystywane znacznie szerzej ze względu na jego przydatność do programowania równoległego.
Ray Hidayat
@Ray: Dodałbym również programowanie rozproszone!
Anton Tykhyy
Większość z nich jest prawdą, z wyjątkiem debugowania. Generalnie jest to trudniejsze w haskell, ponieważ nie masz prawdziwego stosu wywołań, tylko stos dopasowany do wzorca. Dużo trudniej jest przewidzieć, jak skończy się Twój kod.
hasufell
3
Ponadto w programowaniu funkcjonalnym tak naprawdę nie chodzi o „bezstanowe”. Rekursja jest już stanem domniemanym (lokalnym) i jest najważniejszą rzeczą, którą robimy w haskell. Staje się to jasne, kiedy zaimplementujesz kilka nietrywialnych algorytmów w idiomatycznym haskelu (np. Geometria obliczeniowa) i będziesz się dobrze bawić debugowaniem ich.
hasufell
2
Nie lubię utożsamiać bezpaństwowców z PR. Wiele programów PR jest wypełnionych stanem, po prostu istnieje w zamknięciu, a nie w obiekcie.
mikemaccana
46

Im więcej elementów twojego programu jest bezpaństwowych, tym więcej jest sposobów na łączenie elementów bez zerwania . Siła paradygmatu bezpaństwowości nie polega na bezpaństwowości (lub czystości) per se , ale na zdolności, jaką daje on do pisania potężnych funkcji wielokrotnego użytku i łączenia ich.

Dobry samouczek z wieloma przykładami można znaleźć w artykule Johna Hughesa Why Functional Programming Matters (PDF).

Będziesz gęby bardziej produktywne, zwłaszcza jeśli wybrać język funkcjonalny, który posiada również algebraiczne typy danych i dopasowywanie wzorca (Caml, SML, Haskell).

Norman Ramsey
źródło
1
Czy mixin nie zapewniałby również kodu wielokrotnego użytku w podobny sposób z OOP? Nie popieram OOP tylko próbując zrozumieć rzeczy samemu.
mikemaccana
20

Wiele innych odpowiedzi skupiało się na stronie wydajności (równoległości) programowania funkcjonalnego, co moim zdaniem jest bardzo ważne. Jednak zapytałeś konkretnie o produktywność, na przykład, czy możesz zaprogramować to samo szybciej w paradygmacie funkcjonalnym niż w imperatywnym.

Właściwie (z własnego doświadczenia) uważam, że programowanie w języku F # pasuje do sposobu, w jaki myślę, lepiej, więc jest łatwiej. Myślę, że to największa różnica. Programowałem zarówno w F #, jak i C #, aw F # jest dużo mniej „walki z językiem”, co uwielbiam. Nie musisz myśleć o szczegółach w F #. Oto kilka przykładów tego, co naprawdę mi się podoba.

Na przykład, mimo że F # jest wpisywany statycznie (wszystkie typy są rozwiązywane w czasie kompilacji), wnioskowanie o typie określa, jakie typy masz, więc nie musisz tego mówić. A jeśli nie może tego zrozumieć, automatycznie tworzy funkcję / klasę / cokolwiek ogólnego. Więc nigdy nie musisz pisać żadnych ogólnych, wszystko dzieje się automatycznie. Uważam, że oznacza to, że spędzam więcej czasu na myśleniu o problemie, a mniej na tym, jak go wdrożyć. W rzeczywistości za każdym razem, gdy wracam do C #, odkrywam, że naprawdę tęsknię za tego typu wnioskami, nigdy nie zdajesz sobie sprawy, jak rozpraszające to jest, dopóki nie musisz już tego robić.

Również w języku F #, zamiast pisać pętle, wywołujesz funkcje. To subtelna zmiana, ale znacząca, ponieważ nie musisz już myśleć o konstrukcji pętli. Na przykład, oto fragment kodu, który mógłby przejść i dopasować coś (nie pamiętam co, to z projektu układanki Euler):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Zdaję sobie sprawę, że zrobienie filtru, a potem mapy (czyli konwersji każdego elementu) w C # byłoby całkiem proste, ale trzeba myśleć na niższym poziomie. W szczególności musiałbyś napisać samą pętlę i mieć własną wyraźną instrukcję if i tego typu rzeczy. Od kiedy nauczyłem się F #, zdałem sobie sprawę, że łatwiej jest kodować w sposób funkcjonalny, gdzie jeśli chcesz filtrować, piszesz „filtr”, a jeśli chcesz mapować, piszesz „mapa”, zamiast implementować każdy szczegół.

Uwielbiam także operator |>, który moim zdaniem oddziela F # od ocaml i być może innych języków funkcjonalnych. Jest to operator potoku, który pozwala ci "potokować" wyjście jednego wyrażenia do wejścia innego wyrażenia. To sprawia, że ​​kod podąża za tym, jak myślę. Podobnie jak w powyższym fragmencie kodu, to mówi: „weź sekwencję czynników, przefiltruj ją, a następnie zmapuj”. To bardzo wysoki poziom myślenia, którego nie opanujesz w imperatywnym języku programowania, ponieważ jesteś tak zajęty pisaniem pętli i instrukcji if. To jedyna rzecz, której najbardziej brakuje mi, gdy przechodzę na inny język.

Tak więc ogólnie, mimo że mogę programować zarówno w C #, jak i F #, łatwiej mi jest używać F #, ponieważ można myśleć na wyższym poziomie. Twierdzę, że ponieważ mniejsze szczegóły zostały usunięte z programowania funkcjonalnego (przynajmniej w F #), jestem bardziej produktywny.

Edycja : Widziałem w jednym z komentarzy, że prosiłeś o przykład „stanu” w funkcjonalnym języku programowania. F # można napisać imperatywnie, więc oto bezpośredni przykład tego, jak można mieć zmienny stan w F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i
Ray Hidayat
źródło
1
Generalnie zgadzam się z twoim postem, ale |> nie ma nic wspólnego z programowaniem funkcjonalnym jako takim. Właściwie a |> b (p1, p2)to tylko cukier syntaktyczny b (a, p1, p2). Połącz to z prawidłowym skojarzeniem i masz to.
Anton Tykhyy
2
To prawda, powinienem przyznać, że prawdopodobnie wiele moich pozytywnych doświadczeń z F # ma więcej wspólnego z F # niż z programowaniem funkcjonalnym. Ale nadal istnieje silna korelacja między nimi i chociaż rzeczy takie jak wnioskowanie o typie i |> nie są programowaniem funkcjonalnym jako takim, z pewnością twierdzę, że „idą z terytorium”. Przynajmniej ogólnie.
Ray Hidayat
|> to po prostu kolejna funkcja wrostkowa wyższego rzędu, w tym przypadku operator funkcji-aplikacji. Definiowanie własnych operatorów wrostków wyższego rzędu jest zdecydowanie częścią programowania funkcjonalnego (chyba że jesteś Schemerem). Haskell ma swój $, który jest taki sam, z wyjątkiem tego, że informacje w potoku przepływają od prawej do lewej.
Norman Ramsey
15

Rozważ wszystkie trudne błędy, nad którymi debugowałeś przez długi czas.

Ile z tych błędów było spowodowanych „niezamierzonymi interakcjami” między dwoma oddzielnymi komponentami programu? (Prawie wszystkie błędy gwintowania mieć tego formularza: wyścigi obejmujące pisanie współdzielonych danych, zakleszczenia, ... Dodatkowo, powszechne jest znaleźć biblioteki, które mają jakiś nieoczekiwany wpływ na stan globalnej lub odczytu / zapisu rejestru / środowisko, etc.) I zakładałoby, że co najmniej 1 na 3 „twarde błędy” należy do tej kategorii.

Teraz, jeśli przełączysz się na programowanie bezstanowe / niezmienne / czyste, wszystkie te błędy znikną. Zamiast tego pojawiają się nowe wyzwania (np. Gdy chcesz , aby różne moduły wchodziły w interakcję ze środowiskiem), ale w języku takim jak Haskell te interakcje są jawnie reifikowane w systemie typów, co oznacza, że ​​możesz po prostu spojrzeć na typ funkcja i powód dotyczący rodzaju interakcji, jakie może mieć z resztą programu.

To wielka wygrana z IMO „niezmienności”. W idealnym świecie wszyscy projektowalibyśmy wspaniałe interfejsy API, a nawet gdy rzeczy byłyby zmienne, efekty byłyby lokalne i dobrze udokumentowane, a „nieoczekiwane” interakcje byłyby ograniczone do minimum. W prawdziwym świecie istnieje wiele interfejsów API, które współdziałają ze stanem globalnym na niezliczone sposoby i są one źródłem najbardziej zgubnych błędów. Aspirowanie do bezpaństwowości jest dążeniem do pozbycia się niezamierzonych / niejawnych / zakulisowych interakcji między komponentami.

Brian
źródło
6
Ktoś kiedyś powiedział, że nadpisywanie wartości zmiennej oznacza, że ​​jawnie zbierasz / zwalniasz poprzednią wartość. W niektórych przypadkach inne części programu nie zostały wykonane przy użyciu tej wartości. Gdy wartości nie można mutować, ta klasa błędów również znika.
shapr
8

Jedną z zalet funkcji bezstanowych jest to, że umożliwiają wstępne obliczanie lub buforowanie wartości zwracanych przez funkcję. Nawet niektóre kompilatory C pozwalają na jawne oznaczanie funkcji jako bezstanowych, aby poprawić ich optymalizację. Jak wielu innych zauważyło, funkcje bezstanowe są znacznie łatwiejsze do zrównoleglenia.

Ale wydajność nie jest jedynym problemem. Czysta funkcja jest łatwiejsza do testowania i debugowania, ponieważ wszystko, co na nią wpływa, jest wyraźnie określone. A kiedy programuje się w języku funkcjonalnym, przyzwyczaić się do „zabrudzenia” jak najmniejszej liczby funkcji (z I / O itp.). Oddzielenie w ten sposób elementów stanowych jest dobrym sposobem na projektowanie programów, nawet w mniej funkcjonalnych językach.

„Uzyskanie” języków funkcjonalnych może zająć trochę czasu i trudno jest to wytłumaczyć komuś, kto nie przeszedł przez ten proces. Jednak większość ludzi, którzy wytrwali wystarczająco długo, w końcu zdaje sobie sprawę, że warto robić zamieszanie, nawet jeśli nie używają zbyt często języków funkcjonalnych.

Artelius
źródło
Ta pierwsza część jest naprawdę interesująca, nigdy wcześniej o tym nie myślałem. Dzięki!
Sasha Chedygov
Załóżmy, że masz sin(PI/3)w swoim kodzie, gdzie PI jest stałą, kompilator może ocenić tę funkcję w czasie kompilacji i osadzić wynik w wygenerowanym kodzie.
Artelius
6

Bez stanu bardzo łatwo jest automatycznie zrównoleglać kod (ponieważ procesory są wykonane z coraz większą liczbą rdzeni, jest to bardzo ważne).

Zifre
źródło
Tak, zdecydowanie się temu przyjrzałem. W szczególności model współbieżności Erlanga jest bardzo intrygujący. Jednak w tym momencie tak naprawdę nie zależy mi na współbieżności tak bardzo, jak na produktywności. Czy programowanie bez stanu zapewnia premię do produktywności?
Sasha Chedygov
2
@musicfreak, nie, nie ma premii za produktywność. Ale uwaga, współczesne języki FP nadal pozwalają ci używać stanu, jeśli naprawdę go potrzebujesz.
Nieznany
Naprawdę? Czy możesz podać przykład stanu w języku funkcjonalnym, żeby zobaczyć, jak to się robi?
Sasha Chedygov
Sprawdź State monady w Haskell - book.realworldhaskell.org/read/monads.html#x_NZ
zerwa
4
@Unknown: Nie zgadzam się. Programowanie bez stanu ogranicza występowanie błędów, które są spowodowane nieprzewidzianymi / niezamierzonymi interakcjami różnych komponentów. Zachęca również do lepszego projektowania (więcej możliwości ponownego wykorzystania, oddzielenie mechanizmu od polityki i tego typu rzeczy). Nie zawsze jest odpowiedni do danego zadania, ale w niektórych przypadkach naprawdę świeci.
Artelius
6

Bezstanowe aplikacje internetowe są niezbędne, gdy zaczynasz mieć większy ruch.

Może istnieć wiele danych użytkownika, których nie chcesz przechowywać po stronie klienta, na przykład ze względów bezpieczeństwa. W takim przypadku musisz przechowywać go po stronie serwera. Możesz użyć domyślnej sesji aplikacji internetowych, ale jeśli masz więcej niż jedną instancję aplikacji, musisz upewnić się, że każdy użytkownik jest zawsze kierowany do tej samej instancji.

Systemy równoważenia obciążenia często mają możliwość tworzenia `` lepkich sesji '', podczas których system równoważenia obciążenia wie, do którego serwera wysłać żądanie użytkownika. Nie jest to jednak idealne rozwiązanie, na przykład oznacza, że ​​za każdym razem, gdy ponownie uruchomisz aplikację internetową, wszyscy podłączeni użytkownicy stracą sesję.

Lepszym podejściem jest przechowywanie sesji za serwerami sieciowymi w jakimś magazynie danych, obecnie dostępnych jest wiele świetnych produktów nosql (redis, mongo, flexiblesearch, memcached). W ten sposób serwery internetowe są bezstanowe, ale nadal masz stan po stronie serwera, a dostępnością tego stanu można zarządzać, wybierając odpowiednią konfigurację magazynu danych. Te magazyny danych zwykle mają dużą nadmiarowość, więc prawie zawsze powinno być możliwe wprowadzanie zmian w aplikacji internetowej, a nawet w magazynie danych, bez wpływu na użytkowników.

shmish111
źródło