Radzenie sobie z problemami stanu w programowaniu funkcjonalnym

18

Nauczyłem się programować przede wszystkim z punktu widzenia OOP (jak większość z nas, jestem pewien), ale spędziłem dużo czasu próbując nauczyć się, jak rozwiązywać problemy w sposób funkcjonalny. Dobrze rozumiem, jak rozwiązywać problemy obliczeniowe z FP, ale jeśli chodzi o bardziej skomplikowane problemy, zawsze wracam do potrzeb zmiennych obiektów. Na przykład, jeśli piszę symulator cząstek, chcę zaktualizować „obiekty” cząstek ze zmienną pozycją. W jaki sposób z natury „stanowe” problemy są zazwyczaj rozwiązywane za pomocą funkcjonalnych technik programowania?

Andrew Martin
źródło
4
Pierwszym krokiem jest prawdopodobnie uświadomienie sobie, że problemy nie są z natury stanowe.
Telastyn
4
Niektóre problemy są z natury stanowe, takie jak pisanie do bazy danych lub rysowanie GUI. Biorąc przykład z mojego symulatora cząstek, jaki byłby alternatywny sposób myślenia o tym? Zwracanie nowych cząstek za każdym razem, gdy ich pozycja aktualizuje się w celu uniknięcia stanu, wydaje mi się nieefektywne, a nie dobrym modelem realnego świata.
Andrew Martin,
4
Z wyjątkiem być może na przykład bazy danych problemy te nie są z natury stanowe. Na przykład do programowania GUI naprawdę używasz stanu zmiennego jako złego, niejawnego modelu czasu ; funkcjonalne programowanie reaktywne pozwala jawnie modelować czas bez polegania na stanie, zapewniając strumienie zdarzeń, które można łączyć.
Tikhon Jelvis
1
Istnieje prostsze rozwiązanie: jeśli dojdziesz do problemu, który nie jest łatwo modelowany technikami FP, nie używaj programowania funkcjonalnego do jego rozwiązania. Odpowiednie narzędzie do pracy i tak dalej ...
Mason Wheeler,
1
@AndrewMartin Nie jest to dobry model prawdziwego świata? Matematyka stosowana w fizyce do modelowania świata rzeczywistego jest czysto funkcjonalna. Przy dobrym śmieciarzu przydzielanie obiektu jest tak tanie, jak uderzanie wskaźnika, a czas zbierania jest proporcjonalny do liczby obiektów aktywnych . Jeśli już, założę się, że głównym źródłem nieefektywności w programowaniu funkcjonalnym jest stosowanie struktur danych, które nie są wydajne dla pamięci podręcznej. Powiązane listy i drzewa binarne nie są dokładnie potomkami wydajności pamięci podręcznej.
Doval

Odpowiedzi:

20

Programy funkcjonalne bardzo dobrze radzą sobie ze stanem, ale wymagają innego spojrzenia na to. Na przykład w przypadku pozycji należy wziąć pod uwagę fakt, że pozycja jest funkcją czasu zamiast stałej wartości . Działa to dobrze w przypadku cząstek podążających ustaloną ścieżką matematyczną, ale wymagana jest inna strategia radzenia sobie ze zmianą ścieżki, na przykład po zderzeniu.

Podstawową strategią jest tutaj tworzenie funkcji, które przyjmują stan i zwracają nowy stan . Tak więc symulator cząstek byłby funkcją, która pobiera Setcząstkę na wejściu i zwraca nową Setcząstkę po pewnym czasie. Następnie po prostu wielokrotnie wywołujesz tę funkcję, gdy jej wejście jest ustawione na poprzedni wynik.

Karl Bielefeldt
źródło
5
+1 Można mieć stan w FP, po prostu nie można go zmienić .
jhewlett,
1
Dzięki za ten wgląd. Moje obawy o nieefektywność zostały pokrzyżowane przez @logc; szczegóły techniczne transformacji stanu to problem implementacyjny niskiego poziomu, który sam język powinien rozwiązać. W filmie widziałem Richa Hickeya, w jaki sposób robi to z Clojure.
Andrew Martin,
1
@jhewlett: Mówiąc dokładniej: FP ma stan, nawet stan zmienny, ale nie reprezentują go przy użyciu zmiennych zmiennych.
Giorgio
9

Jak zauważył @KarlBielefeldt, funkcjonalnym podejściem do takiego problemu jest postrzeganie go jako powrotu do nowego stanu z poprzedniego stanu. Same funkcje nie przechowują żadnych informacji, dlatego zawsze aktualizują stan m do stanu n .

Myślę, że uważasz to za nieskuteczne, ponieważ zakładasz, że poprzedni stan musi być przechowywany w pamięci podczas obliczania nowego stanu. Zauważ, że wybór pomiędzy pisaniem całkowicie nowego stanu a ponownym pisaniem starego na miejscu jest szczegółem implementacji z punktu widzenia funkcjonalnego języka.

Powiedzmy na przykład, że mam listę milionów liczb całkowitych i chcę zwiększyć dziesiątą o jedną jednostkę. Kopiowanie całej listy z nowym numerem na dziesiątej pozycji jest marnotrawstwem, masz rację; ale jest to tylko koncepcyjny sposób opisania operacji kompilatorowi języka lub tłumaczowi. Kompilator lub interpreter może pobrać pierwszą listę i po prostu nadpisać dziesiątą pozycję.

Zaletą opisania operacji w ten sposób jest to, że kompilator może uzasadnić sytuację, gdy wiele wątków chce zaktualizować tę samą listę w różnych pozycjach. Jeśli operacja jest opisana jako „przejdź do tej pozycji i nadpisz to, co znalazłeś”, to programista, a nie kompilator, jest odpowiedzialny za to, aby nadpisania nie kolidowały.

Biorąc to wszystko pod uwagę, nawet w Haskell istnieje monada stanowa, która pomaga modelować sytuacje, w których „utrzymywanie stanu” jest bardziej intuicyjnym rozwiązaniem problemu. Ale proszę również zauważyć, że niektóre problemy, które uważasz za „z natury stanowe, jak pisanie do bazy danych ” mają niezmienne rozwiązania, takie jak Datomic . Może to być zaskakujące, dopóki nie zrozumiesz, że jest to koncepcja, niekoniecznie jej realizacja.

logc
źródło
4
Myślę, że krótki fragment dotyczący aktualizacji dużej listy jest mylący; Nie znam żadnego kompilatora, który faktycznie wykonałby dla ciebie optymalizację. Nawet jeśli kompilator mógłby to zrobić, jest to możliwe tylko w przypadkach, w których nie trzymasz się poprzednich wersji listy. Prawdziwym rozwiązaniem jest użycie struktury lista danych, które nie wymaga kopiowania całość zmienić pojedynczy element.
Doval
@Doval: „Nawet jeśli kompilator mógłby to zrobić, jest to możliwe tylko w przypadkach, w których nie trzymasz się poprzednich wersji listy.”: Przypomina mi to unikalne typy w Clean.
Giorgio
4

Subskrybowanie właściwego modelu mentalnego pomaga lepiej myśleć i zarządzać stanem. Moim zdaniem najlepszym modelem mentalnym jest flipbook . Po kliknięciu zrozumiesz, że FP mocno opiera się na trwałych strukturach danych, które przechwytują stan świata i że funkcje są używane do przejścia tego stanu bez jakiejkolwiek mutacji.

Rich Hickey przedstawia te pomysły:

inne rozmowy, ale to powinno skierować cię we właściwym kierunku.

Mario T. Lanza
źródło
3

Pisząc duże i umiarkowanie duże aplikacje, często uważałem za użyteczne rozróżnienie między częściami aplikacji, które są stanowe, a tymi, które są bezstanowe.

Klasy / struktury danych w sekcji stanowej przechowują dane aplikacji, a funkcje w tej sekcji działają z niejawną wiedzą o danych aplikacji.

Klasy / struktury danych / funkcje w sekcji bezstanowej służą do obsługi czysto algorytmicznych aspektów aplikacji. Nie mają niejawnej wiedzy na temat danych aplikacji. Działają w charakterze czysto funkcjonalnym. W stanowych częściach aplikacji może wystąpić zmiana stanu jako efekt uboczny uruchamiania funkcji w bezstanowej sekcji aplikacji.

Najtrudniejsze jest ustalenie, które klasy / funkcje należy umieścić w sekcji bezstanowej, a które klasy / funkcje, które należy umieścić w sekcji stanowej, oraz dyscypliny, aby umieścić je w osobnych plikach / bibliotekach.

R Sahu
źródło
Jak to odpowiada na pytanie? (not-downvoting)
kravemir
@kravemir, niezależnie od tego, czy piszesz aplikację przy użyciu OOP, czy FP, musisz zrozumieć, gdzie znajduje się stan aplikacji.
R Sahu