Ich versuche zu verstehen, wie man mit unveränderlichen Daten in FP umgeht (speziell in F #, aber andere FPs sind auch in Ordnung) und die alte Gewohnheit des staatlich vollständigen Denkens (OOP-Stil) zu brechen. Ein Teil der ausgewählten Antwort auf die hier gestellte Frage wiederholte meine Suche nach möglichen Beschreibungen von Problemen, die durch zustandsbehaftete Darstellungen in OOP mit unveränderlichen in FP gelöst werden (zum Beispiel: Eine Warteschlange bei Produzenten und Verbrauchern). Irgendwelche Gedanken oder Links sind willkommen? Danke im Voraus.
Bearbeiten : Um die Frage ein wenig näher zu erläutern, wie würden unveränderliche Strukturen (z. B. Warteschlange) in FP auf mehrere Threads (z. B. Produzent und Konsument) gleichzeitig aufgeteilt
quelle
Antworten:
Obwohl dies manchmal so ausgedrückt wird, verhindert die funktionale Programmierung¹ keine Zustandsberechnungen. Was es tut, ist, den Programmierer zu zwingen, den Zustand explizit zu machen.
Nehmen wir zum Beispiel die Grundstruktur eines Programms mit einer imperativen Warteschlange (in einigen Pseudosprachen):
Die entsprechende Struktur mit einer funktionalen Warteschlangendatenstruktur (immer noch in einer imperativen Sprache, um jeweils einen Unterschied anzugehen) würde folgendermaßen aussehen:
Da die Warteschlange jetzt unveränderlich ist, ändert sich das Objekt selbst nicht. In diesem Pseudocode ist
q
selbst eine Variable; die Aufgabenq := Queue.add(…)
undq := tail
machen es auf ein anderes Objekt zeigen. Die Schnittstelle der Queue-Funktionen hat sich geändert: Jedes muss das neue Queue-Objekt zurückgeben, das sich aus der Operation ergibt.In einer rein funktionalen Sprache, dh in einer Sprache ohne Nebeneffekte, müssen Sie all state explizit machen. Da Produzent und Konsument vermutlich etwas tun, muss auch hier ihr Zustand in der Aufruferschnittstelle stehen.
Beachten Sie, wie jetzt jeder Zustand explizit verwaltet wird. Die Warteschlangenmanipulationsfunktionen nehmen eine Warteschlange als Eingabe und erzeugen eine neue Warteschlange als Ausgabe. Der Produzent und der Konsument geben auch ihren Staat durch.
Die gleichzeitige Programmierung passt nicht so gut in die funktionale Programmierung, passt aber sehr gut in die Umgebung funktionale Programmierung. Die Idee ist, eine Reihe separater Rechenknoten auszuführen und sie Nachrichten austauschen zu lassen. Jeder Knoten führt ein Funktionsprogramm aus, und sein Status ändert sich beim Senden und Empfangen von Nachrichten.
Wenn Sie das Beispiel fortsetzen, wird eine einzelne Warteschlange von einem bestimmten Knoten verwaltet. Verbraucher senden diesem Knoten eine Nachricht, um ein Element zu erhalten. Produzenten senden diesem Knoten eine Nachricht, um ein Element hinzuzufügen.
Die einzige „industrialisierte“ Sprache, bei der die Parallelität stimmt³, ist Erlang . Das Erlernen von Erlang ist definitiv der Weg zur Aufklärung über die gleichzeitige Programmierung.
Jetzt schalten alle auf nebenwirkungsfreie Sprachen um!
¹ Dieser Begriff hat mehrere Bedeutungen. Ich denke, Sie meinen damit Programmieren ohne Nebenwirkungen, und das ist die Bedeutung, die ich auch benutze.
² Die Programmierung mit implizitem Status ist eine zwingende Programmierung . Objektorientierung ist ein völlig orthogonales Anliegen.
³ Entzündlich, ich weiß, aber ich meine es ernst. Threads mit Shared Memory sind die Assemblersprache für die gleichzeitige Programmierung. Die Weitergabe von Nachrichten ist viel einfacher zu verstehen, und das Fehlen von Nebenwirkungen wird deutlich, sobald Sie die Parallelität einführen.
⁴ Und das kommt von jemandem, der kein Fan von Erlang ist, aber aus anderen Gründen.
quelle
Das zustandsbehaftete Verhalten in einer FP-Sprache wird als Transformation von einem vorherigen Zustand in einen neuen Zustand implementiert. Eine Warteschlange ist beispielsweise eine Umwandlung von einer Warteschlange und einem Wert in eine neue Warteschlange mit dem Wert in der Warteschlange. Die Warteschlange ist eine Umwandlung von einer Warteschlange in einen Wert und eine neue Warteschlange, bei der der Wert entfernt wird. Konstrukte wie Monaden wurden entwickelt, um diese Zustandsumwandlung (und andere Ergebnisse der Berechnung) auf nützliche Weise zu abstrahieren
quelle
Ihre Frage ist, was als "XY-Problem" bezeichnet wird. Insbesondere ist das von Ihnen angegebene Konzept (Warteschlange bei Produzenten und Verbrauchern) tatsächlich eine Lösung und kein "Problem", wie Sie es beschreiben. Dies führt zu einer Schwierigkeit, da Sie nach einer rein funktionalen Implementierung von etwas fragen, das von Natur aus unrein ist. Meine Antwort beginnt mit einer Frage: Welches Problem möchten Sie lösen?
Es gibt viele Möglichkeiten, wie mehrere Produzenten ihre Ergebnisse an einen gemeinsamen Verbraucher senden können. Die vielleicht naheliegendste Lösung in F # besteht darin, den Verbraucher zu einem Agenten (aka
MailboxProcessor
) zu machen und die ProduzentenPost
ihre Ergebnisse dem Konsumentenagenten zur Verfügung zu stellen. Dies verwendet intern eine Warteschlange und ist nicht rein (das Senden von Nachrichten in F # ist ein unkontrollierter Nebeneffekt, eine Verunreinigung).Es ist jedoch sehr wahrscheinlich, dass das zugrunde liegende Problem eher dem Scatter-Gather-Muster aus der parallelen Programmierung entspricht. Um dieses Problem zu lösen, können Sie ein Array von Eingabewerten erstellen und diese dann
Array.Parallel.map
überlagern und die Ergebnisse mithilfe einer Reihe zusammenstellenArray.reduce
. Alternativ können Sie Funktionen aus demPSeq
Modul verwenden, um die Elemente von Sequenzen parallel zu verarbeiten.Ich möchte auch betonen, dass an staatlichem Denken nichts auszusetzen ist. Reinheit hat Vorteile, ist aber kein Allheilmittel, und Sie sollten sich auch seiner Mängel bewusst werden. Genau aus diesem Grund ist F # keine reine Funktionssprache: Sie können also Verunreinigungen verwenden, wenn diese vorzuziehen sind.
quelle
Clojure hat ein sehr gut durchdachtes Konzept von Staat und Identität, das eng mit der Parallelität zusammenhängt. Die Unveränderlichkeit spielt eine wichtige Rolle. Alle Werte in Clojure sind unveränderlich und können über Referenzen aufgerufen werden. Referenzen sind mehr als nur einfache Zeiger. Sie verwalten den Zugriff auf Werte, und es gibt mehrere Typen mit unterschiedlicher Semantik. Eine Referenz kann geändert werden, um auf einen neuen (unveränderlichen) Wert zu verweisen, und eine solche Änderung ist garantiert atomar. Nach der Änderung arbeiten jedoch alle anderen Threads immer noch mit dem ursprünglichen Wert, zumindest bis sie wieder auf die Referenz zugreifen.
Ich empfehle Ihnen nachdrücklich, einen ausgezeichneten Artikel über Staat und Identität in Clojure zu lesen , der die Details viel besser erklärt, als ich es könnte.
quelle