2013-03-18 4 views
53

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.

+0

Sie suchen nach funktionaler reaktiver Programmierung – luqui

+4

[_ 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. –

+0

Sie können immer noch sagen, was 'updateMonkeys' tut, da es' :: State -> Monkey -> State' ist –

Antwort

29

Die Antwort ist functional reactive programming (FRP). Es ist eine Mischung aus zwei Codierungsstilen: Komponentenstatusverwaltung und zeitabhängige Werte. Da FRP eigentlich eine ganze Familie von Designmustern ist, möchte ich genauer sein: Ich empfehle Netwire.

Die zugrunde liegende Idee ist sehr einfach: Sie schreiben viele kleine, in sich geschlossene Komponenten mit jeweils einem eigenen lokalen Zustand. Dies entspricht praktisch den zeitabhängigen Werten, da Sie bei jeder Abfrage einer solchen Komponente möglicherweise eine andere Antwort erhalten und eine lokale Statusaktualisierung auslösen. Dann kombinieren Sie diese Komponenten zu Ihrem eigentlichen Programm.

Obwohl das kompliziert und ineffizient klingt, ist es eigentlich nur eine sehr dünne Schicht um reguläre Funktionen. Das Designmuster von Netwire wurde von AFRP (Arrowized Functional Reactive Programming) inspiriert. Es ist wahrscheinlich anders genug, um einen eigenen Namen zu verdienen (WFRP?). Vielleicht möchten Sie die tutorial lesen.

In jedem Fall folgt eine kleine Demo. Ihre Bausteine ​​sind Drähte:

myWire :: WireP A B 

Betrachten Sie dies als eine Komponente. Es ist ein zeitveränderliche Wert des Typs B, die auf einem zeitlich veränderlichen Wert des Typs hängt A, beispielsweise einen Teilchens in einem Simulator:

particle :: WireP [Particle] Particle 

es auf einer Liste von Teilchen abhängt (für Beispiel alle aktuell vorhandenen Partikel) und ist selbst ein Partikel. Lassen Sie uns einen vorgegebenen Draht verwenden (mit einem vereinfachten Typ):

time :: WireP a Time 

Dies ist ein zeitlich veränderlichen Wert vom Typ Zeit (= Doppel). Nun, es ist die Zeit selbst (beginnend mit 0 beginnend ab wann das Kabelnetzwerk gestartet wurde). Da es nicht auf einen anderen zeitveränderlichen Wert ankommt, können Sie es beliebig einspeisen, daher der polymorphe Eingabetyp. Darüber hinaus gibt es konstante Drähte (zeitlich veränderlichen Werte, die nicht im Laufe der Zeit ändern):

pure 15 :: Wire a Integer 

-- or even: 
15 :: Wire a Integer 

Um zwei Drähte verbinden Sie einfach mit kategorischer Zusammensetzung:

integral_ 3 . 15 

Dies gibt Ihnen eine Uhr bei 15x Echtzeitgeschwindigkeit (das Integral von 15 über die Zeit) beginnend bei 3 (die Integrationskonstante). Dank verschiedener Klasseninstanzen sind Kabel sehr praktisch zu kombinieren. Sie können Ihre regulären Operatoren sowie Anwendungsstil oder Pfeilstil verwenden. Willst du eine Uhr, die bei 10 beginnt und zweimal die Echtzeitgeschwindigkeit ist?

10 + 2*time 

einen Partikel möchten, die beginnen, und (0, 0) mit (0, 0) Geschwindigkeit und beschleunigt mit (2, 1) pro Sekunde pro Sekunde?

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

Möchten Sie Statistiken anzeigen, während der Benutzer die Leertaste drückt?

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

Dies ist nur ein kleiner Bruchteil dessen, was Netwire für Sie tun kann.

+4

Danke! Sie machen gute Argumente für FRP. Ich hatte es vorher gehört, und ich war aufgeregt. Aber dann stieß ich auf einige Definitionen, die behaupteten, FRP sei im Wesentlichen identisch mit den automatisch aktualisierenden Formeln in MS Excel. Das hat mich dazu gebracht, es zu vernachlässigen, weshalb ich es in meiner Frage nicht erwähnt habe. Es war besonders hilfreich, dass Sie erwähnt haben, dass es sehr unterschiedliche Vorstellungen darüber gibt, was FRP in der Praxis bedeutet. Das hatte ich bis jetzt nicht bemerkt. – rlkw1024

1

Ich weiß, das ist altes Thema. Aber ich stehe gerade dem gleichen Problem gegenüber, während ich versuche, Rail Fence Cipher von exercism.io zu implementieren. Es ist ziemlich enttäuschend zu sehen, dass solch ein häufiges Problem in Haskell so wenig Beachtung findet. Ich nehme es nicht, um etwas so einfach zu machen, wie Zustand beizubehalten, ich muss FRP lernen. Also, ich googelte weiter und fand eine Lösung, die direkter aussah - State monad: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State

+0

Die Frage erwähnt bereits die Staatsmonade ("Einige würden vorschlagen, dass wir GameState in einer State Monad einpacken ..."). Das Problem ist, wie man einen riesigen globalen Staat vermeidet, in dem jeder Teil des Programms funktioniert. –