Angenommen, Sie erstellen eine ziemlich große Simulation in Haskell. Es gibt viele verschiedene Arten von Entitäten, deren Attribute sich im Verlauf der Simulation aktualisieren. Lassen Sie uns beispielsweise sagen, dass Ihre Entitäten Affen, Elefanten, Bären usw. genannt werden.Behalten des komplexen Zustands in Haskell
Was ist Ihre bevorzugte Methode, um die Zustände dieser Entitäten zu verwalten?
Der erste und offensichtlichste Ansatz, den ich gedacht war:
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'
Es ist schon hässlich jede Art von Unternehmen, die in der mainLoop
Funktion Unterschrift ausdrücklich erwähnt. Sie können sich vorstellen, wie es absolut schrecklich werden würde, wenn Sie, sagen wir, 20 Arten von Entitäten hätten. (20 ist für komplexe Simulationen nicht unangemessen.) Ich halte das für einen inakzeptablen Ansatz. Aber seine Rettung ist, dass Funktionen wie updateMonkeys
sehr explizit sind in was sie tun: Sie nehmen eine Liste von Affen und geben eine neue zurück.
Also dann der nächste Gedanke wäre alles in eine große Datenstruktur zu rollen, die alle Zustand hält, so dass die Unterschrift von mainLoop
Reinigung:
mainLoop :: GameState -> String
mainLoop gs0 =
let gs1 = updateMonkeys gs0
gs2 = updateElephants gs1
gs3 = updateBears gs2
in
if shouldExit gs0 then "Done" else
mainLoop gs3
Einige würden vorschlagen, dass wir GameState
wickeln in einem Staat nach oben Monad und rufen Sie updateMonkeys
usw. in einem do
. Das ist gut. Einige würden eher vorschlagen, dass wir es mit Funktionszusammensetzung aufräumen. Auch gut, denke ich. (BTW, ich bin ein Anfänger mit Haskell, also bin ich vielleicht etwas falsch.)
Aber dann ist das Problem, Funktionen wie updateMonkeys
nicht geben Sie nützliche Informationen aus ihrer Typ-Signatur. Sie können nicht wirklich sicher sein, was sie tun. Sicher, updateMonkeys
ist ein beschreibender Name, aber das ist wenig Trost. Wenn ich eine god object gebe und sage "Bitte aktualisieren Sie meinen globalen Zustand", fühle ich mich wie in der imperativen Welt. Es fühlt sich an wie globale Variablen unter einem anderen Namen: Sie haben eine Funktion, die etwas in den globalen Zustand, Sie nennen es, und Sie hoffen auf das Beste. (Ich nehme an, Sie vermeiden immer noch Probleme mit Nebenläufigkeit, die bei globalen Variablen in einem imperativen Programm vorhanden wären. Aber meh, Nebenläufigkeit ist bei globalen Variablen nicht annähernd das einzige.)
Ein weiteres Problem ist das: Angenommen, die Objekte müssen interagieren. Zum Beispiel haben wir eine Funktion wie folgt aus:
stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
(elongateEvilGrin elephant, decrementHealth monkey)
Sagen Sie diesen in updateElephants
aufgerufen wird, denn das ist, wo wir überprüfen, um zu sehen, ob der Elefanten in Stampfen Bereich von irgendwelchen Affen sind. Wie verbreitet man elegant die Veränderungen an Affen und Elefanten in diesem Szenario? In unserem zweiten Beispiel nimmt updateElephants
ein god-Objekt an und gibt es zurück, so dass es beide Änderungen bewirken kann. Aber das verwirrt nur die Gewässer und verstärkt meinen Standpunkt: Mit dem Gott-Objekt mutieren Sie effektiv nur globale Variablen. Und wenn Sie das god-Objekt nicht verwenden, bin ich nicht sicher, wie Sie diese Arten von Änderungen propagieren würden.
Was ist zu tun? Sicherlich müssen viele Programme den komplexen Zustand verwalten, also vermute ich, dass es einige bekannte Ansätze für dieses Problem gibt.
Nur zum Vergleich, hier ist, wie ich das Problem in der OOP-Welt lösen könnte. Es würde Monkey
, Elephant
usw. Objekte geben. Ich würde wahrscheinlich Klassenmethoden haben, um Nachschlagewerke in der Menge aller lebenden Tiere zu machen. Vielleicht könntest du nach Ort suchen, nach ID, was auch immer.Dank der Datenstrukturen, die den Suchfunktionen zugrunde liegen, bleiben sie auf dem Heap reserviert. (Ich nehme GC oder Referenzzählung an.) Ihre Mitgliedsvariablen würden die ganze Zeit mutieren. Jede Methode jeder Klasse könnte jedes lebende Tier einer anderen Klasse mutieren. Z.B. ein Elephant
könnte eine stomp
Methode, die die Gesundheit eines übergebenen in Monkey
Objekt verringern würde, und es gäbe keine Notwendigkeit, dass
Ebenfalls in einer Erlang oder anderen Akteur-orientiertes Design, übergeben werden, können Sie diese Probleme lösen könnte ziemlich elegant: Jeder Akteur behält seine eigene Schleife und damit seinen eigenen Zustand, so dass man nie ein Gott-Objekt braucht. Und die Nachrichtenübergabe ermöglicht es den Aktivitäten eines Objekts, Änderungen in anderen Objekten auszulösen, ohne einen Stapel von Dingen den gesamten Stapel hindurch zurückgeben zu müssen. Aber ich habe gehört, dass Schauspieler in Haskell verpönt sind.
Sie suchen nach funktionaler reaktiver Programmierung – luqui
[_ Purely Functional, Deklarative Spiellogik mit Reactive Programming_] (https://github.com/leonidas/codeblog /blob/master/2012/2012-01-17-declarative-game-logic-afrp.md) kann Sie in die richtige Richtung weisen. –
Sie können immer noch sagen, was 'updateMonkeys' tut, da es' :: State -> Monkey -> State' ist –