2013-03-18 12 views
53

Załóżmy, że budujesz dość dużą symulację w Haskell. Istnieje wiele różnych typów jednostek, których atrybuty są aktualizowane w trakcie trwania symulacji. Załóżmy na przykład, że twoje istoty nazywają się Małpy, Słonie, Niedźwiedzie itp.Utrzymywanie złożonego stanu w Haskell

Jaka jest Twoja preferowana metoda utrzymywania stanów tych jednostek?

Pierwszym i najbardziej oczywistym podejście myślałem było to:

mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String 
mainLoop monkeys elephants bears = 
    let monkeys' = updateMonkeys monkeys 
     elephants' = updateElephants elephants 
     bears'  = updateBears  bears 
    in 
    if shouldExit monkeys elephants bears then "Done" else 
     mainLoop monkeys' elephants' bears' 

To już brzydki mający każdy typ jednostki wyraźnie wymienione w podpisie mainLoop funkcyjnego. Możesz sobie wyobrazić, jak byłoby absolutnie okropnie, gdybyś miał na przykład 20 rodzajów bytów. (20 nie jest nierozsądne w przypadku złożonych symulacji). Uważam, że jest to niedopuszczalne podejście. Ale jego oszczędność polega na tym, że funkcje takie jak updateMonkeys są bardzo wyraźne w tym, co robią: Biorą listę małp i zwracają nową.

Więc następna myśl byłoby rzucić wszystko w jeden wielki struktury danych, która przechowuje wszystkie państwa, w ten sposób oczyszczania podpis mainLoop:

mainLoop :: GameState -> String 
mainLoop gs0 = 
    let gs1 = updateMonkeys gs0 
     gs2 = updateElephants gs1 
     gs3 = updateBears  gs2 
    in 
    if shouldExit gs0 then "Done" else 
     mainLoop gs3 

Niektórzy sugerują, że możemy owinąć GameState się w państwie Monada i zadzwoń pod numer updateMonkeys itp. W do. W porządku. Niektórzy wolą zaproponować, abyśmy posprzątali go składem funkcji. Również dobrze, myślę. (BTW, jestem nowicjuszem z Haskellem, więc może się mylę co do tego).

Ale problem polega na tym, że funkcje takie jak updateMonkeys nie dostarczają użytecznych informacji z ich podpisów. Naprawdę nie możesz być pewien, co robią. Oczywiście, updateMonkeys to nazwa opisowa, ale to małe pocieszenie. Kiedy przekazuję god object i mówię "proszę zaktualizuj mój globalny stan", czuję, że wróciliśmy do imperatywnego świata. Czuje się jak zmienne globalne pod inną nazwą: Masz funkcję, która ma coś do stanu globalnego, nazywasz to i masz nadzieję na najlepsze. (Przypuszczam, że nadal unikasz problemów współbieżności, które byłyby obecne w zmiennych globalnych w imperatywnym programie, ale współdziałanie nie jest jedyną rzeczą, która jest nie tak ze zmiennymi globalnymi.)

Kolejny problem to: Załóżmy, że obiekty muszą wchodzić w interakcje. Na przykład, mamy funkcję tak:

stomp :: Elephant -> Monkey -> (Elephant, Monkey) 
stomp elephant monkey = 
    (elongateEvilGrin elephant, decrementHealth monkey) 

Wypowiedz to jest wywoływana w updateElephants, bo tam możemy sprawdzić, czy któryś z słonie są w zakresie depcząc jakichkolwiek małp. W jaki sposób w tym scenariuszu elegancko propagujesz zmiany zarówno u małp jak i słoni? W naszym drugim przykładzie, updateElephants pobiera i zwraca obiekt boga, aby mógł dokonać obu zmian. Ale to tylko pogłębia wodę i wzmacnia mój punkt widzenia: dzięki obiektowi boga skutecznie mutujesz zmienne globalne. A jeśli nie używasz obiektu bożego, nie jestem pewien, w jaki sposób możesz propagować te typy zmian.

Co robić? Z pewnością wiele programów wymaga zarządzania złożonym stanem, więc zgaduję, że istnieje kilka dobrze znanych podejść do tego problemu.

Tylko dla porównania, oto jak mogę rozwiązać problem w świecie OOP. Będą obiekty Monkey, Elephant itd. Najprawdopodobniej miałbym metody klasowe do wyszukiwania odnośników w zestawie wszystkich żywych zwierząt. Może mógłbyś szukać według lokalizacji, według ID, cokolwiek.Dzięki strukturom danych stanowiącym podstawę funkcji wyszukiwania, zostaną one przydzielone na stercie. (Zakładam, że GC lub liczenie odniesień.) Ich zmienne składowe byłyby cały czas zmutowane. Każda metoda jakiejkolwiek klasy będzie w stanie mutować dowolne żywe zwierzę dowolnej innej klasy. Na przykład. Elephant może mieć stomp metodę, która byłaby Decrement zdrowia zdanym w Monkey obiektu, a nie byłoby potrzeby, aby przekazać tę

Podobnie w Erlang lub innego projektu aktora zorientowanych, można rozwiązać te problemy dość elegancko: Każdy aktor zachowuje własną pętlę, a tym samym swój własny stan, więc nigdy nie potrzebujesz obiektu bożego. Przekazywanie komunikatów pozwala aktywom jednego obiektu wywoływać zmiany w innych obiektach bez przepuszczania całej wiązki rzeczy z powrotem do stosu wywołań. Jednak słyszałem, jak mówiono, że aktorzy w Haskell są mile zaskoczeni.

+0

Szukasz funkcjonalnego programowania reaktywnego – luqui

+4

[_Pewna funkcjonalna, deklaratywna logika gry za pomocą programowania reaktywnego_] (https://github.com/leonidas/codeblog /blob/master/2012/2012-01-17-declarative-game-logic-afrp.md) może wskazać ci właściwy kierunek. –

+0

Nadal można stwierdzić, co robi 'updateMonkeys', ponieważ jest to' :: State -> Monkey -> State' –

Odpowiedz

29

Odpowiedź brzmi: functional reactive programming (FRP). Jest to hybryda dwóch stylów kodowania: zarządzanie stanem komponentów i wartości zależne od czasu. Ponieważ FRP to w rzeczywistości cała rodzina wzorców projektowych, chcę być bardziej szczegółowy: polecam Netwire.

Podstawowy pomysł jest bardzo prosty: piszesz wiele małych, samodzielnych komponentów, każdy z własnym stanem lokalnym. Jest to praktycznie równoznaczne z wartościami zależnymi od czasu, ponieważ za każdym razem, gdy pytasz o taki komponent, możesz uzyskać inną odpowiedź i spowodować aktualizację stanu lokalnego. Następnie łączysz te komponenty, aby utworzyć rzeczywisty program.

Chociaż brzmi to skomplikowanie i nieefektywnie, w rzeczywistości jest to bardzo cienka warstwa wokół zwykłych funkcji. Wzór projektu wdrożony przez Netwire został zainspirowany przez AFRP (Arrowed Functional Reactive Programming). Prawdopodobnie jest na tyle różny, że zasługuje na własną nazwę (WFRP?). Możesz przeczytać tutorial.

W każdym razie pojawia się małe demo. Twoje bloki konstrukcyjne to przewody:

myWire :: WireP A B 

Pomyśl o tym jako o komponencie. Jest wartością zależną od czasu typu B, który zależy od wartości zmiennej w czasie typu , na przykład cząstek na symulatorze:

particle :: WireP [Particle] Particle 

Zależy listy cząstek (na przykład wszystkie obecnie istniejące cząstki) i sam jest cząstką. Użyjmy predefiniowany drutu (o uproszczonej typu):

time :: WireP a Time 

Jest to wartość zmiennych w czasie typu Czas (= Pokój). Cóż, już czas (od 0 liczony od momentu uruchomienia sieci przewodowej). Ponieważ nie zależy to od innej zmiennej czasowej, możesz ją podać dowolnie, stąd polimorficzny typ wejścia. Są też stałe przewody (wartości zmiennych w czasie, które nie zmieniają się w czasie):

pure 15 :: Wire a Integer 

-- or even: 
15 :: Wire a Integer 

Aby połączyć dwa przewody po prostu użyć kategoryczne skład:

integral_ 3 . 15 

Daje to zegar na 15x prędkość w czasie rzeczywistym (całkowita 15 z czasem) zaczynająca się od 3 (stała całkowania). Dzięki różnym klasom przewody są bardzo wygodne w łączeniu. Możesz korzystać ze swoich zwykłych operatorów, jak i stylu aplikacji lub stylu strzałki. Chcesz zegar, który zaczyna się od 10 i jest dwa razy większa od prędkości w czasie rzeczywistym?

10 + 2*time 

chcesz cząstkę, która rozpoczyna się i (0, 0) o (0, 0), prędkość i przyspieszenie w (2, 1) na sekundę, na sekundę?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1) 

Chcesz wyświetlać statystyki, gdy użytkownik naciśnie spację?

stats . keyDown Spacebar <|> "stats currently disabled" 

To tylko niewielka część tego, co Netwire może dla Ciebie zrobić.

+4

Dzięki! Sprawiasz, że masz dobre prawo do FRP. Wcześniej słyszałem, jak to sugerowało i byłem podekscytowany. Ale wtedy natknąłem się na niektóre definicje, twierdząc, że FRP było zasadniczo identyczne z formułami automatycznej aktualizacji w MS Excel. To skłoniło mnie do zdyskontowania go, dlatego nie wspomniałem o tym w moim pytaniu. Szczególnie pomocne było wspomnienie, jak bardzo różne są koncepcje dotyczące tego, co FRP oznacza w praktyce. Nie zdawałem sobie z tego sprawy aż dotąd. – rlkw1024

1

Wiem, że to stary temat. Ale mam teraz ten sam problem, próbując wdrożyć ćwiczenie szyfrowe Rail Fence z exercism.io. To dość rozczarowujące, że tak powszechny problem ma tak nikłą uwagę w Haskell. Nie uważam, że aby zrobić coś tak prostego, jak utrzymanie stanu, muszę nauczyć się FRP. Kontynuowałem więc szukanie google i znalazłem rozwiązanie bardziej proste - State Monad: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State

+0

Pytanie już wspomina o monadzie państwowej ("Niektórzy sugerowaliby, że zawijamy GameState w Monadę państwową ..."). Problem polega na tym, jak uniknąć gigantycznego globalnego stanu, w którym działa każda część programu. –

Powiązane problemy