Ich bin zugegebenermaßen voreingenommen, wenn ich solche Konzepte in C ++ anwende, und zwar aufgrund der Sprache und ihrer Natur sowie meiner Domäne und sogar der Art und Weise, wie wir die Sprache verwenden. Angesichts dieser Umstände halte ich unveränderliche Designs für den uninteressantesten Aspekt, wenn es darum geht, einen Großteil der mit der funktionalen Programmierung verbundenen Vorteile zu nutzen, z kombiniere sie in beliebiger Reihenfolge ohne unangenehme Überraschungen), etc.
Nehmen Sie dieses vereinfachte C ++ - Beispiel (zugegebenermaßen nicht der Einfachheit halber optimiert, um mich nicht vor irgendwelchen Bildverarbeitungsexperten in Verlegenheit zu bringen):
// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
Image dst(new_w, new_h);
for (int y=0; y < new_h; ++y)
{
for (int x=0; x < new_w; ++x)
dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
}
return dst;
}
Während die Implementierung dieser Funktion den lokalen (und temporären) Zustand in Form von zwei Zählervariablen und einem auszugebenden temporären lokalen Bild verändert, hat sie keine externen Nebenwirkungen. Es wird ein Bild eingegeben und ein neues ausgegeben. Wir können es nach Herzenslust multithreaden. Es ist leicht zu überlegen, leicht gründlich zu testen. Es ist ausnahmesicher, da das neue Bild automatisch verworfen wird und wir uns keine Gedanken über das Zurücksetzen externer Nebenwirkungen machen müssen (es werden sozusagen keine externen Bilder außerhalb des Funktionsbereichs geändert).
Ich sehe wenig zu gewinnen und möglicherweise viel zu verlieren, wenn ich Image
in C ++ im obigen Kontext unveränderlich mache , außer um die obige Funktion möglicherweise unhandlicher und möglicherweise ein bisschen weniger effizient zu implementieren.
Reinheit
Daher sind reine Funktionen (frei von externen Nebenwirkungen) für mich sehr interessant, und ich betone, wie wichtig es ist, sie auch in C ++ Teammitgliedern häufig vorzuziehen. Aber unveränderliche Entwürfe, die nur im Allgemeinen ohne Kontext und ohne Nuancen angewendet werden, sind für mich bei weitem nicht so interessant, da es angesichts des imperativen Charakters der Sprache oft nützlich und praktisch ist, in der Lage zu sein, einige lokale temporäre Objekte auf effiziente Weise zu mutieren (beides) für Entwickler und Hardware) eine reine Funktion implementieren.
Günstiges Kopieren von schweren Strukturen
Die zweitnützlichste Eigenschaft, die ich finde, ist die Fähigkeit, die wirklich umfangreichen Datenstrukturen billig zu kopieren, wenn die Kosten dafür, wie sie häufig anfallen würden, um Funktionen aufgrund ihrer strengen Eingabe / Ausgabe-Natur rein zu machen, nicht trivial wären. Dies wären keine kleinen Strukturen, die auf den Stapel passen. Das wären große, kräftige Strukturen, wie das Ganze Scene
für ein Videospiel.
In diesem Fall könnte der Kopieraufwand Möglichkeiten für eine effektive Parallelisierung verhindern, da es schwierig sein kann, Physik und Rendering effektiv zu parallelisieren, ohne sich gegenseitig zu sperren und einen Engpass zu verursachen, wenn die Physik die Szene mutiert, die der Renderer gleichzeitig zu zeichnen versucht, während er gleichzeitig eine tiefe Physik aufweist Das Kopieren der gesamten Spielszene, um nur ein Einzelbild mit angewandter Physik auszugeben, kann gleichermaßen ineffektiv sein. Wenn das physikalische System jedoch in dem Sinne "rein" wäre, dass es lediglich eine Szene eingibt und eine neue mit angewandter Physik ausgibt, und eine solche Reinheit nicht auf Kosten des astronomischen Kopieraufwands gehen würde, könnte es sicher parallel mit dem System arbeiten Renderer, ohne dass einer auf den anderen wartet.
Die Möglichkeit, die wirklich umfangreichen Daten Ihres Anwendungszustands kostengünstig zu kopieren und neue, geänderte Versionen mit minimalen Kosten für Verarbeitung und Speichernutzung auszugeben, kann also wirklich neue Türen für Reinheit und effektive Parallelität öffnen, und dort finde ich viele Lektionen zum Lernen davon, wie persistente Datenstrukturen implementiert werden. Was auch immer wir mit solchen Lektionen erstellen, muss nicht vollständig persistent sein oder unveränderliche Schnittstellen bieten (es kann beispielsweise Copy-on-Write oder ein "Builder / Transient" verwendet werden), um diese Fähigkeit zu erreichen, spottbillig zu sein nur Teile der Kopie kopieren und ändern, ohne die Speichernutzung und den Speicherzugriff zu verdoppeln, um Parallelität und Reinheit in unseren Funktionen / Systemen / Pipelines zu erreichen.
Unveränderlichkeit
Schließlich gibt es eine Unveränderlichkeit, die ich als die am wenigsten interessante von diesen dreien betrachte, die sich jedoch mit eiserner Faust durchsetzen kann, wenn bestimmte Objektdesigns nicht als lokale temporäre Elemente für eine reine Funktion, sondern in einem breiteren Kontext als wertvoll angesehen werden sollen Art von "Reinheit auf Objektebene", da in allen Methoden keine externen Nebenwirkungen mehr auftreten (keine Mutation der Mitgliedsvariablen mehr außerhalb des unmittelbaren lokalen Bereichs der Methode).
Und obwohl ich es in Sprachen wie C ++ für das am wenigsten interessante unter diesen dreien halte, kann es sicherlich das Testen und die Thread-Sicherheit und die Argumentation von nicht-trivialen Objekten vereinfachen. Es kann sich lohnen, mit der Garantie zu arbeiten, dass einem Objekt beispielsweise keine eindeutige Zustandskombination außerhalb seines Konstruktors zugewiesen werden kann, und dass wir es auch per Referenz / Zeiger frei weitergeben können, ohne uns auf Beständigkeit und Lesbarkeit zu stützen. nur Iteratoren und Handles und so, während garantiert wird (na ja, so viel wie möglich innerhalb der Sprache), dass sein ursprünglicher Inhalt nicht mutiert wird.
Aber ich finde das die am wenigsten interessante Eigenschaft, weil die meisten Objekte, die ich als nützlich erachte, vorübergehend in veränderlicher Form verwendet werden, um eine reine Funktion zu implementieren (oder sogar ein breiteres Konzept, wie ein "reines System", das ein Objekt oder eine Reihe von Objekten sein könnte) funktioniert letztendlich so, dass nur etwas eingegeben und etwas Neues ausgegeben wird, ohne etwas anderes zu berühren), und ich halte die Unveränderlichkeit, die in einer weitgehend imperativen Sprache bis zum Äußersten getragen wird, für ein eher kontraproduktives Ziel. Ich würde es sparsam für die Teile der Codebasis anwenden, wo es wirklich am meisten hilft.
Schließlich:
Es scheint, dass persistente Datenstrukturen an sich nicht ausreichen, um Szenarien zu bewältigen, in denen ein Thread eine Änderung vornimmt, die für andere Threads sichtbar ist. Dazu müssen wir anscheinend Geräte wie Atome, Referenzen, Software-Transaktionsspeicher oder sogar klassische Schlösser und Synchronisationsmechanismen verwenden.
Wenn in Ihrem Design Änderungen (im Sinne eines benutzerorientierten Designs) für mehrere Threads gleichzeitig sichtbar sein sollen, kehren wir natürlich zur Synchronisierung zurück, oder zumindest zum Zeichenbrett, um einige ausgefeilte Methoden zu finden, um damit umzugehen ( Ich habe einige sehr ausführliche Beispiele gesehen, die von Experten verwendet wurden, die sich mit solchen Problemen in der funktionalen Programmierung befassten.
Aber ich habe herausgefunden, dass, wenn Sie diese Art des Kopierens und die Fähigkeit haben, teilweise modifizierte Versionen von massiven Strukturen spottbillig auszugeben, wie Sie es zum Beispiel mit persistenten Datenstrukturen tun würden, es Ihnen oft viele Türen und Möglichkeiten öffnet Ich habe noch nie darüber nachgedacht, Code zu parallelisieren, der in einer strengen I / O-Art paralleler Pipeline völlig unabhängig voneinander laufen kann. Selbst wenn einige Teile des Algorithmus serieller Natur sein müssen, können Sie diese Verarbeitung auf einen einzelnen Thread verschieben. Wenn Sie sich jedoch auf diese Konzepte stützen, können Sie problemlos 90% der umfangreichen Arbeit parallelisieren, z