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.
Szukasz funkcjonalnego programowania reaktywnego – luqui
[_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. –
Nadal można stwierdzić, co robi 'updateMonkeys', ponieważ jest to' :: State -> Monkey -> State' –