Ich kann sehen, dass die Vorteile von veränderlichen gegenüber unveränderlichen Objekten wie unveränderlichen Objekten aufgrund des gemeinsamen und beschreibbaren Status viele schwer zu behebende Probleme bei der Multithread-Programmierung beseitigen. Im Gegenteil, veränderbare Objekte helfen dabei, mit der Identität des Objekts umzugehen, anstatt jedes Mal eine neue Kopie zu erstellen, und verbessern somit auch die Leistung und die Speichernutzung, insbesondere für größere Objekte.
Eine Sache, die ich zu verstehen versuche, ist, was schief gehen kann, wenn veränderbare Objekte im Kontext der funktionalen Programmierung vorhanden sind. Wie einer der Punkte, die mir gesagt wurden, ist, dass das Ergebnis des Aufrufs von Funktionen in unterschiedlicher Reihenfolge nicht deterministisch ist.
Ich suche nach einem konkreten Beispiel, bei dem sehr offensichtlich ist, was mit veränderlichen Objekten in der Funktionsprogrammierung schief gehen kann. Grundsätzlich ist es schlecht, wenn es schlecht ist, unabhängig von OO oder funktionalem Programmierparadigma, oder?
Ich glaube unten meine eigene Aussage selbst beantwortet diese Frage. Trotzdem brauche ich ein Beispiel, damit ich es natürlicher fühlen kann.
OO hilft dabei, Abhängigkeiten zu verwalten und einfachere und wartbarere Programme mit Hilfe von Tools wie Kapselung, Polymorphismus usw. zu schreiben.
Funktionale Programmierung hat auch das gleiche Motiv, wartbaren Code zu fördern, aber durch die Verwendung eines Stils, der die Verwendung von OO-Tools und -Techniken überflüssig macht - eine davon ist meines Erachtens die Minimierung von Nebenwirkungen, reiner Funktion usw.
Antworten:
Ich denke, die Bedeutung lässt sich am besten anhand eines OO-Ansatzes demonstrieren
Angenommen, wir haben ein Objekt
Im OO-Paradigma ist die Methode an die Daten angehängt, und es ist sinnvoll, diese Daten durch die Methode zu mutieren.
Im Funktionsparadigma definieren wir ein Ergebnis in Bezug auf die Funktion. ein gekauftes um IS das Ergebnis der Kauffunktion auf einen Auftrag angewendet. Dies impliziert einige Dinge, bei denen wir uns sicher sein müssen
Würden Sie order.Status == "Purchased" erwarten?
Dies impliziert auch, dass unsere Funktionen idempotent sind. dh. Wenn Sie sie zweimal ausführen, sollte jedes Mal das gleiche Ergebnis erzielt werden.
Wenn die Bestellung durch die Kauffunktion geändert wurde, schlug purchaseOrder2 fehl.
Indem wir Dinge als Ergebnisse von Funktionen definieren, können wir diese Ergebnisse verwenden, ohne sie tatsächlich zu berechnen. Was in Bezug auf die Programmierung eine verzögerte Ausführung ist.
Dies kann an sich praktisch sein, aber wenn wir uns nicht sicher sind, wann eine Funktion tatsächlich ausgeführt wird UND wir damit einverstanden sind, können wir die Parallelverarbeitung viel stärker nutzen als in einem OO-Paradigma.
Wir wissen, dass das Ausführen einer Funktion die Ergebnisse einer anderen Funktion nicht beeinflusst. So können wir den Computer verlassen, um sie in beliebiger Reihenfolge auszuführen, wobei so viele Threads verwendet werden, wie er möchte.
Wenn eine Funktion ihre Eingabe mutiert, müssen wir bei solchen Dingen viel vorsichtiger sein.
quelle
Order Purchase() { return new Order(Status = "Purchased") }
, dass der Status schreibgeschützt ist. ? Warum ist diese Praxis im Kontext des Funktionsprogrammierparadigmas relevanter? Die von Ihnen erwähnten Vorteile zeigen sich auch in der OO-Programmierung, oder?Der Schlüssel zum Verständnis, warum unveränderliche Objekte von Vorteil sind, liegt nicht wirklich darin, konkrete Beispiele im Funktionscode zu finden. Da der größte Teil des Funktionscodes in funktionalen Sprachen geschrieben ist und die meisten funktionalen Sprachen standardmäßig unveränderlich sind, soll das Paradigma verhindern, dass das passiert, wonach Sie suchen.
Der Schlüssel zu fragen ist, was ist der Vorteil der Unveränderlichkeit? Die Antwort ist, es vermeidet Komplexität. Angenommen, wir haben zwei Variablen
x
undy
. Beide beginnen mit dem Wert von1
.y
verdoppelt sich jedoch alle 13 Sekunden. Was wird der Wert von jedem von ihnen in 20 Tagen sein?x
wird sein1
. Das ist leicht. Es würde allerdings Mühe kosten, dies herauszufinden,y
da es viel komplexer ist. Welche Tageszeit in 20 Tagen? Muss ich die Sommerzeit berücksichtigen? Die Komplexität vony
versusx
ist einfach so viel mehr.Und das kommt auch in echtem Code vor. Jedes Mal, wenn Sie der Mischung einen mutierenden Wert hinzufügen, wird dies zu einem weiteren komplexen Wert, den Sie im Kopf oder auf Papier halten und berechnen müssen, wenn Sie versuchen, den Code zu schreiben, zu lesen oder zu debuggen. Je komplexer, desto größer ist die Wahrscheinlichkeit, dass Sie einen Fehler machen und einen Fehler einführen. Der Code ist schwer zu schreiben; schwer zu lesen; Schwer zu debuggen: Der Code ist schwer richtig zu machen.
Veränderlichkeit ist jedoch nicht schlecht . Ein Programm ohne Veränderlichkeit kann kein Ergebnis haben, was ziemlich nutzlos ist. Selbst wenn die Veränderlichkeit darin besteht, ein Ergebnis auf einen Bildschirm, eine Festplatte oder was auch immer zu schreiben, muss es vorhanden sein. Was schlecht ist, ist unnötige Komplexität. Eine der einfachsten Möglichkeiten, die Komplexität zu reduzieren, besteht darin, Dinge standardmäßig unveränderlich zu machen und sie aus Leistungs- oder Funktionsgründen nur bei Bedarf veränderbar zu machen.
quelle
y
muss mutieren; das ist eine Voraussetzung. Manchmal müssen wir komplexen Code haben, um komplexe Anforderungen zu erfüllen. Der Punkt, den ich ansprechen wollte, ist, dass unnötige Komplexität vermieden werden sollte. Mutierende Werte sind von Natur aus komplexer als feste Werte. Um unnötige Komplexität zu vermeiden, mutieren Sie Werte nur dann, wenn dies erforderlich ist.Die gleichen Dinge, die bei der nicht funktionierenden Programmierung schief gehen können: Sie können unerwünschte, unerwartete Nebenwirkungen bekommen , was eine bekannte Fehlerursache seit der Erfindung der Programmiersprachen mit Gültigkeitsbereich ist.
Meiner Meinung nach besteht der einzige wirkliche Unterschied zwischen funktionaler und nicht funktionaler Programmierung darin, dass Sie bei nicht funktionalem Code normalerweise Nebenwirkungen erwarten, bei funktionaler Programmierung jedoch nicht.
Sicher - unerwünschte Nebenwirkungen sind eine Kategorie von Fehlern, unabhängig vom Paradigma. Das Gegenteil ist auch der Fall - absichtlich verwendete Nebenwirkungen können helfen, Leistungsprobleme zu lösen, und sind in der Regel für die meisten realen Programme erforderlich, wenn es um E / A und den Umgang mit externen Systemen geht - auch unabhängig vom Paradigma.
quelle
Ich habe gerade eine StackOverflow-Frage beantwortet , die Ihre Frage ziemlich gut veranschaulicht. Das Hauptproblem bei veränderlichen Datenstrukturen besteht darin, dass ihre Identität nur zu einem bestimmten Zeitpunkt gültig ist. Daher neigen die Benutzer dazu, so viel wie möglich in den kleinen Punkt im Code zu stopfen, an dem sie wissen, dass die Identität konstant ist. In diesem Beispiel wird viel in einer for-Schleife protokolliert:
Wenn Sie an Unveränderlichkeit gewöhnt sind, besteht keine Angst, dass sich die Datenstruktur ändert, wenn Sie zu lange warten, sodass Sie Aufgaben, die logisch getrennt sind, viel entkoppelter erledigen können:
quelle
Der Vorteil der Verwendung unveränderlicher Objekte besteht darin, dass man einfach übergeben kann, wenn man eine Referenz auf ein Objekt erhält, das eine bestimmte Eigenschaft hat, wenn der Empfänger es untersucht, und einem anderen Code eine Referenz auf ein Objekt mit derselben Eigenschaft geben muss entlang der Referenz auf das Objekt, ohne Rücksicht darauf, wer sonst die Referenz erhalten hat oder was sie mit dem Objekt tun könnten [da niemand etwas mit dem Objekt tun kann ] oder wenn der Empfänger das Objekt untersuchen könnte [da alle seine Eigenschaften sind gleich, unabhängig davon, wann sie untersucht werden].
Im Gegensatz dazu muss Code, der jemandem einen Verweis auf ein veränderliches Objekt geben muss, das eine bestimmte Eigenschaft hat, wenn der Empfänger es untersucht (vorausgesetzt, der Empfänger selbst ändert es nicht), entweder wissen, dass sich nichts anderes als der Empfänger jemals ändern wird diese Eigenschaft oder wissen, wann der Empfänger auf diese Eigenschaft zugreifen wird, und wissen, dass nichts diese Eigenschaft ändern wird, bis der Empfänger sie das letzte Mal untersucht.
Ich denke, es ist am hilfreichsten, für die Programmierung im Allgemeinen (nicht nur für die funktionale Programmierung) unveränderliche Objekte in drei Kategorien zu unterteilen:
Objekte, die nichts zulassen können, können sie auch mit einer Referenz nicht ändern. Solche Objekte und Verweise auf sie verhalten sich wie Werte und können frei geteilt werden.
Objekte, die sich durch Code ändern lassen würden, auf den verwiesen wird, deren Verweise jedoch niemals einem Code ausgesetzt werden, der sie tatsächlich ändern würde. Diese Objekte kapseln Werte, können jedoch nur mit vertrauenswürdigem Code geteilt werden, um sie nicht zu ändern oder sie dem Code auszusetzen, der möglicherweise funktioniert.
Objekte, die geändert werden. Diese Objekte werden am besten als Container betrachtet und als Bezeichner bezeichnet .
Ein nützliches Muster besteht häufig darin, dass ein Objekt einen Container erstellt, ihn mit Code füllt, dem vertraut werden kann, dass er danach keine Referenz mehr aufbewahrt, und dass dann die einzigen Referenzen, die jemals irgendwo im Universum existieren, in Code enthalten sind, der den Code niemals ändert Objekt, sobald es gefüllt ist. Während der Container von einem veränderlichen Typ sein könnte, kann er über (*) begründet werden, als ob er unveränderlich wäre, da nichts ihn jemals mutieren wird. Wenn alle Verweise auf den Container in unveränderlichen Wrapper-Typen aufbewahrt werden, deren Inhalt sich niemals ändert, können solche Wrapper sicher weitergegeben werden, als ob die darin enthaltenen Daten in unveränderlichen Objekten gespeichert wären, da Verweise auf die Wrapper frei geteilt und geprüft werden können Jederzeit.
(*) In Multithread-Code kann es erforderlich sein, "Speicherbarrieren" zu verwenden, um sicherzustellen, dass die Auswirkungen aller Aktionen auf den Container für diesen Thread sichtbar sind, bevor ein Thread möglicherweise einen Verweis auf den Wrapper sehen kann Dies ist ein Sonderfall, der hier nur der Vollständigkeit halber erwähnt wird.
quelle
Wie bereits erwähnt, ist das Problem mit dem veränderlichen Zustand im Grunde eine Unterklasse des größeren Problems der Nebenwirkungen , bei dem der Rückgabetyp einer Funktion nicht genau beschreibt, was die Funktion wirklich tut, da sie in diesem Fall auch eine Zustandsmutation durchführt. Dieses Problem wurde von einigen neuen Forschungssprachen wie F * ( http://www.fstar-lang.org/tutorial/ ) behoben . Diese Sprache schafft eine Wirkung System auf das Typ - System ähnlich, in dem eine Funktion nicht nur statisch seinen Typen deklariert, sondern auch seine Auswirkungen. Auf diese Weise wissen die Aufrufer der Funktion, dass beim Aufrufen der Funktion eine Zustandsmutation auftreten kann und dieser Effekt an ihre Aufrufer weitergegeben wird.
quelle