Ich lese und höre, dass die Leute (auch auf dieser Seite) routinemäßig das Paradigma der funktionalen Programmierung loben und betonen, wie gut es ist, alles unveränderlich zu haben. Insbesondere schlagen die Leute diesen Ansatz sogar in traditionell zwingenden OO-Sprachen wie C #, Java oder C ++ vor, nicht nur in rein funktionalen Sprachen wie Haskell, die dies dem Programmierer aufzwingen.
Ich finde es schwer zu verstehen, weil ich Veränderlichkeit und Nebenwirkungen ... praktisch finde. Angesichts der Art und Weise, wie Menschen derzeit Nebenwirkungen verurteilen und es für eine gute Praxis halten, sie nach Möglichkeit loszuwerden, glaube ich jedoch, dass ich, wenn ich ein kompetenter Programmierer sein möchte, meine Karriere beginnen muss, um das Paradigma besser zu verstehen ... Daher mein Q.
Ein Ort, an dem ich Probleme mit dem Funktionsparadigma finde, ist, wenn ein Objekt natürlich von mehreren Stellen aus referenziert wird. Lassen Sie es mich an zwei Beispielen beschreiben.
Das erste Beispiel ist mein C # -Spiel, das ich in meiner Freizeit machen möchte . Es ist ein rundenbasiertes Web-Spiel, bei dem beide Spieler Teams mit 4 Monstern haben und ein Monster aus ihrem Team ins Spiel schicken können, wo es dem Monster des gegnerischen Spielers gegenübersteht. Spieler können auch Monster vom Schlachtfeld zurückrufen und durch ein anderes Monster aus ihrem Team ersetzen (ähnlich wie bei Pokemon).
In dieser Einstellung kann ein einzelnes Monster natürlich von mindestens zwei Stellen aus referenziert werden: dem Team eines Spielers und dem Schlachtfeld, auf das zwei "aktive" Monster verweisen.
Betrachten wir nun die Situation, in der ein Monster getroffen wird und 20 Gesundheitspunkte verliert. In den Klammern des imperativen Paradigmas modifiziere ich das health
Feld dieses Monsters , um diesen Wandel widerzuspiegeln - und das mache ich jetzt. Dies macht die Monster
Klasse jedoch veränderlich und die damit verbundenen Funktionen (Methoden) unrein, was meiner Meinung nach derzeit als schlechte Praxis angesehen wird.
Obwohl ich mir die Erlaubnis gegeben habe, den Code dieses Spiels in einem nicht idealen Zustand zu haben, um die Hoffnung zu haben, ihn irgendwann in der Zukunft tatsächlich zu beenden, würde ich gerne wissen und verstehen, wie er sein sollte richtig geschrieben. Deshalb: Wenn dies ein Konstruktionsfehler ist, wie kann er behoben werden?
In dem funktionalen Stil, wie ich ihn verstehe, würde ich stattdessen eine Kopie dieses Monster
Objekts erstellen und es bis auf dieses eine Feld mit dem alten identisch halten. und die Methode suffer_hit
würde dieses neue Objekt zurückgeben, anstatt das alte zu ändern. Dann würde ich das Battlefield
Objekt ebenfalls kopieren und alle Felder bis auf dieses Monster gleich halten.
Dies bringt mindestens 2 Schwierigkeiten mit sich:
- Die Hierarchie kann leicht viel tiefer sein als dieses vereinfachte Beispiel von nur
Battlefield
->Monster
. Ich müsste alle Felder außer einem so kopieren und ein neues Objekt bis zum Ende dieser Hierarchie zurückgeben. Dies wäre Boilerplate-Code, den ich besonders ärgerlich finde, da die funktionale Programmierung Boilerplate reduzieren soll. - Ein viel schwerwiegenderes Problem ist jedoch, dass dies dazu führen würde, dass die Daten nicht mehr synchron sind . Das aktive Monster des Feldes würde seine Gesundheit verringern; Das gleiche Monster, auf das von seinem kontrollierenden Spieler verwiesen wird
Team
, würde dies jedoch nicht tun. Wenn ich stattdessen den imperativen Stil annehmen würde, wäre jede Änderung von Daten sofort von allen anderen Stellen des Codes aus sichtbar, und in solchen Fällen wie diesem finde ich es wirklich praktisch - aber die Art und Weise, wie ich Dinge erhalte, ist genau das, was die Leute sagen falsch mit dem imperativen Stil!- Jetzt wäre es möglich, sich um dieses Problem zu kümmern, indem Sie
Team
nach jedem Angriff eine Reise zum unternehmen . Das ist zusätzliche Arbeit. Was aber, wenn ein Monster später plötzlich von noch mehr Orten aus referenziert werden kann? Was ist, wenn ich mit einer Fähigkeit komme, mit der sich ein Monster beispielsweise auf ein anderes Monster konzentrieren kann, das nicht unbedingt auf dem Spielfeld ist (ich denke tatsächlich über eine solche Fähigkeit nach)? Werde ich mich sicher daran erinnern, sofort nach jedem Angriff auch eine Reise zu fokussierten Monstern zu unternehmen? Dies scheint eine Zeitbombe zu sein, die explodieren wird, wenn der Code komplexer wird. Ich denke, es ist keine Lösung.
- Jetzt wäre es möglich, sich um dieses Problem zu kümmern, indem Sie
Eine Idee für eine bessere Lösung stammt aus meinem zweiten Beispiel, als ich auf dasselbe Problem stieß. In der Wissenschaft wurde uns gesagt, wir sollten in Haskell einen Dolmetscher für eine Sprache unseres eigenen Designs schreiben. (Auf diese Weise musste ich auch verstehen, was FP ist). Das Problem trat auf, als ich Schließungen implementierte. Erneut kann derselbe Bereich jetzt von mehreren Stellen aus referenziert werden: Über die Variable, die diesen Bereich enthält, und als übergeordneter Bereich aller verschachtelten Bereiche! Wenn eine Änderung an diesem Bereich über eine der darauf verweisenden Referenzen vorgenommen wird, muss diese Änderung natürlich auch durch alle anderen Referenzen sichtbar sein.
Die Lösung bestand darin, jedem Bereich eine ID zuzuweisen und ein zentrales Wörterbuch aller Bereiche in der State
Monade zu führen. Jetzt würden Variablen nur die ID des Bereichs enthalten, an den sie gebunden waren, und nicht den Bereich selbst, und verschachtelte Bereiche würden auch die ID ihres übergeordneten Bereichs enthalten.
Ich denke, der gleiche Ansatz könnte in meinem Monster-Kampfspiel versucht werden ... Felder und Teams beziehen sich nicht auf Monster; Sie enthalten stattdessen IDs von Monstern, die in einem zentralen Monsterwörterbuch gespeichert sind.
Ich sehe jedoch wieder ein Problem mit diesem Ansatz, das mich daran hindert, es ohne zu zögern als Lösung für das Problem zu akzeptieren:
Es ist wieder eine Quelle für Boilerplate-Code. Einzeiler müssen zwangsläufig dreizeilig sein: Was früher eine einzeilige direkte Änderung eines einzelnen Felds war, erfordert jetzt (a) Abrufen des Objekts aus dem zentralen Wörterbuch (b) Vornehmen der Änderung (c) Speichern des neuen Objekts zum zentralen Wörterbuch. Das Speichern von IDs von Objekten und zentralen Wörterbüchern anstelle von Referenzen erhöht die Komplexität. Da FP beworben wird, um die Komplexität und den Boilerplate-Code zu reduzieren , deutet dies darauf hin, dass ich es falsch mache.
Ich wollte auch über ein zweites Problem schreiben, das viel schwerwiegender zu sein scheint: Dieser Ansatz führt zu Speicherlecks . Objekte, die nicht erreichbar sind, werden normalerweise durch Müll gesammelt. Objekte, die in einem zentralen Wörterbuch gespeichert sind, können jedoch nicht mit Müll gesammelt werden, selbst wenn kein erreichbares Objekt auf diese bestimmte ID verweist. Und während theoretisch sorgfältige Programmierung Speicherlecks vermeiden kann (wir könnten darauf achten, jedes Objekt manuell aus dem zentralen Wörterbuch zu entfernen, sobald es nicht mehr benötigt wird), ist dies fehleranfällig und FP wird angekündigt, um die Korrektheit von Programmen zu erhöhen nicht der richtige Weg sein.
Ich fand jedoch rechtzeitig heraus, dass es eher ein gelöstes Problem zu sein scheint. Java bietet WeakHashMap
, mit denen dieses Problem behoben werden kann. C # bietet eine ähnliche Funktion - ConditionalWeakTable
- obwohl es laut den Dokumenten von Compilern verwendet werden soll. Und in Haskell haben wir System.Mem.Weak .
Ist das Speichern solcher Wörterbücher die richtige funktionale Lösung für dieses Problem oder gibt es eine einfachere, die ich nicht sehe? Ich stelle mir vor, dass die Anzahl solcher Wörterbücher leicht und schlecht wachsen kann; Wenn diese Wörterbücher auch unveränderlich sein sollen, kann dies eine Menge Parameterübergabe oder in Sprachen, die dies unterstützen, monadische Berechnungen bedeuten , da die Wörterbücher in Monaden gehalten würden (aber ich lese das noch einmal in rein funktionaler Form Sprachen, in denen so wenig Code wie möglich vorhanden ist, sollten monadisch sein, während diese Wörterbuchlösung fast den gesamten Code in die State
Monade einfügt. Dies lässt mich erneut bezweifeln, ob dies die richtige Lösung ist.)
Nach einiger Überlegung möchte ich noch eine Frage hinzufügen: Was gewinnen wir durch die Erstellung solcher Wörterbücher? Was bei der imperativen Programmierung falsch ist, ist nach Ansicht vieler Experten, dass Änderungen an einigen Objekten auf andere Codeteile übertragen werden. Um dieses Problem zu lösen, sollten Objekte unveränderlich sein - genau aus diesem Grund sollten, wenn ich richtig verstehe, an ihnen vorgenommene Änderungen an keiner anderen Stelle sichtbar sein. Aber jetzt mache ich mir Sorgen um andere Codeteile, die mit veralteten Daten arbeiten, und erfinde daher zentrale Wörterbücher, damit ... wieder Änderungen in einigen Codeteilen auf andere Codeteile übertragen werden! Kehren wir also nicht zum imperativen Stil mit all seinen vermeintlichen Nachteilen zurück, sondern mit zusätzlicher Komplexität?
quelle
Team
) das Ergebnis des Kampfes und damit die Zustände der Monster durch ein Tupel (Kampfnummer, ID der Monsterentität) abrufen.Antworten:
Wie behandelt die funktionale Programmierung ein Objekt, auf das von mehreren Stellen aus verwiesen wird? Es lädt Sie ein, Ihr Modell erneut zu besuchen!
Zur Erklärung ... schauen wir uns an, wie vernetzte Spiele manchmal geschrieben werden - mit einer zentralen "Golden Source" -Kopie des Spielstatus und einer Reihe eingehender Client-Ereignisse, die diesen Status aktualisieren und dann wieder an die anderen Clients gesendet werden .
Sie können über den Spaß lesen , den das Factorio-Team daran hatte, dass sich dies in bestimmten Situationen gut verhält. Hier ist eine kurze Übersicht über ihr Modell:
Der Schlüssel ist, dass der Status jedes Objekts an dem bestimmten Häkchen in der Zeitleiste unveränderlich ist . Alles im globalen Multiplayer-Zustand muss letztendlich zu einer deterministischen Realität konvergieren.
Und - das könnte der Schlüssel zu Ihrer Frage sein. Der Status jeder Entität ist für einen bestimmten Tick unveränderlich, und Sie verfolgen die Übergangsereignisse, die im Laufe der Zeit neue Instanzen erzeugen.
Wenn Sie darüber nachdenken, muss die vom Server eingehende Ereigniswarteschlange Zugriff auf ein zentrales Verzeichnis von Entitäten haben, damit sie ihre Ereignisse anwenden kann.
Letztendlich sind Ihre einfachen einzeiligen Mutatormethoden, die Sie nicht komplizieren möchten, nur einfach, weil Sie die Zeit nicht wirklich genau modellieren. Wenn Immerhin Gesundheit in der Mitte der Bearbeitungsschleife ändern kann, dann früher Entitäten in dieser Zecke werden einen alten Wert sehen, und spätere ein verändert man sehen. Dies sorgfältig zu handhaben bedeutet, zumindest den aktuellen (unveränderlichen) und den nächsten (im Bau befindlichen) Zustand zu unterscheiden, die wirklich nur zwei Zecken in der großen Zeitlinie der Zecken sind!
Als allgemeine Richtlinie sollten Sie also in Betracht ziehen, den Zustand eines Monsters in eine Reihe kleiner Objekte aufzuteilen, die sich beispielsweise auf Ort / Geschwindigkeit / Physik, Gesundheit / Schaden und Vermögenswerte beziehen. Erstellen Sie ein Ereignis, um jede möglicherweise auftretende Mutation zu beschreiben, und führen Sie Ihre Hauptschleife wie folgt aus:
Oder sowas ähnliches. Ich denke: "Wie würde ich das verteilen?" ist im Allgemeinen eine ziemlich gute mentale Übung, um mein Verständnis zu verfeinern, wenn ich verwirrt bin, wo Dinge leben und wie sie sich entwickeln sollten.
Dank eines Hinweises von @ AaronM.Eshbach wird hervorgehoben, dass dies eine ähnliche Problemdomäne ist wie Event Sourcing und das CQRS- Muster, bei dem Sie Statusänderungen in einem verteilten System als eine Reihe unveränderlicher Ereignisse im Laufe der Zeit modellieren . In diesem Fall versuchen wir höchstwahrscheinlich, eine komplexe Datenbank-App zu bereinigen, indem wir (wie der Name schon sagt!) Die Behandlung des Mutator-Befehls vom Abfrage- / Ansichtssystem trennen. Natürlich komplexer, aber flexibler.
quelle
Du bist immer noch halb im Imperativlager. Anstatt jeweils an ein einzelnes Objekt zu denken, denken Sie an Ihr Spiel in Bezug auf eine Geschichte von Spielen oder Ereignissen
etc
Sie können den Status des Spiels zu einem bestimmten Zeitpunkt berechnen, indem Sie die Aktionen miteinander verketten, um ein unveränderliches Statusobjekt zu erstellen. Jedes Spiel ist eine Funktion, die ein Statusobjekt nimmt und ein neues Statusobjekt zurückgibt
quelle