Was kann im Zusammenhang mit der funktionalen Programmierung schief gehen, wenn mein Objekt veränderlich ist?

9

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.

rahulaga_dev
quelle
1
@Ruben Ich würde sagen, dass die meisten funktionalen Sprachen veränderbare Variablen zulassen, aber es anders machen, sie zu verwenden, z. B. veränderbare Variablen haben einen anderen Typ
jk.
1
Ich denke, Sie haben in Ihrem ersten Absatz möglicherweise unveränderlich und veränderlich gemischt?
jk.
1
@jk., hat er sicherlich getan. Bearbeitet, um das zu korrigieren.
David Arno
6
@ Ruben Funktionale Programmierung ist ein Paradigma. Als solches benötigt es keine funktionierende Programmiersprache. Und einige fp-Sprachen wie F # haben diese Funktion .
Christophe
1
@Ruben nein speziell ich habe an Mvars in haskell hackage.haskell.org/package/base-4.9.1.0/docs/ gedacht. Verschiedene Sprachen haben natürlich unterschiedliche Lösungen oder IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html obwohl Sie natürlich beide innerhalb von Monaden verwenden würden
jk.

Antworten:

7

Ich denke, die Bedeutung lässt sich am besten anhand eines OO-Ansatzes demonstrieren

Angenommen, wir haben ein Objekt

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

Im OO-Paradigma ist die Methode an die Daten angehängt, und es ist sinnvoll, diese Daten durch die Methode zu mutieren.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

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

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

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.

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

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.

Ewan
quelle
Vielen Dank !! sehr hilfreich. Eine neue Implementierung des Kaufs würde also so aussehen 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?
rahulaga_dev
In OO würden Sie erwarten, dass object.Purchase () das Objekt ändert. Sie könnten es unveränderlich machen, aber warum nicht zu einem vollständigen funktionalen Paradigma
übergehen
Ich denke, das Problem muss visualisiert werden, weil ich ein reiner C # -Entwickler bin, der von Natur aus objektorientiert ist. Was Sie also in einer Sprache sagen, die funktionale Programmierung umfasst, erfordert nicht, dass die Funktion 'Purchase ()', die eine Bestellung zurückgibt, mit einer Klasse oder einem Objekt verknüpft wird, oder?
rahulaga_dev
3
Sie können funktionale c # schreiben, um Ihr Objekt in eine Struktur zu ändern, es unveränderlich zu machen und einen Func <Order, Order> Purchase
Ewan
12

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 xund y. Beide beginnen mit dem Wert von 1. yverdoppelt sich jedoch alle 13 Sekunden. Was wird der Wert von jedem von ihnen in 20 Tagen sein? xwird sein 1. Das ist leicht. Es würde allerdings Mühe kosten, dies herauszufinden, yda es viel komplexer ist. Welche Tageszeit in 20 Tagen? Muss ich die Sommerzeit berücksichtigen? Die Komplexität von yversus xist 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.

David Arno
quelle
4
"Eine der einfachsten Möglichkeiten, die Komplexität zu reduzieren, besteht darin, Dinge standardmäßig unveränderlich zu machen und sie nur bei Bedarf veränderbar zu machen": Sehr schöne und prägnante Zusammenfassung.
Giorgio
2
@DavidArno Die von Ihnen beschriebene Komplexität macht es schwierig, über Code nachzudenken. Sie haben dies auch angesprochen, als Sie sagten: "Der Code ist schwer zu schreiben, schwer zu lesen, schwer zu debuggen; ...". Ich mag unveränderliche Objekte, weil sie es viel einfacher machen, über Code nachzudenken, nicht nur für mich selbst, sondern auch für Beobachter, die zuschauen, ohne das gesamte Projekt zu kennen.
Zerlegen-Nummer-5
1
@ RahulAgarwal, " Aber warum wird dieses Problem im Zusammenhang mit der funktionalen Programmierung immer wichtiger ? " Das tut es nicht. Ich denke, ich bin vielleicht verwirrt von dem, was Sie fragen, da das Problem in FP weit weniger ausgeprägt ist, da FP die Unveränderlichkeit fördert und so das Problem vermeidet.
David Arno
1
@djechlin, " Wie kann Ihr 13-Sekunden-Beispiel mit unveränderlichem Code einfacher zu analysieren sein? " Es kann nicht: ymuss 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.
David Arno
3
Veränderlichkeit schafft eine Identitätskrise. Ihre Variable hat keine einzige Identität mehr. Stattdessen hängt seine Identität jetzt von der Zeit ab. Anstelle eines einzelnen x haben wir jetzt symbolisch eine Familie x_t. Jeder Code, der diese Variable verwendet, muss sich jetzt auch um die Zeit kümmern, was zu einer zusätzlichen Komplexität führt, die in der Antwort erwähnt wird.
Alex Vong
8

Was kann im Zusammenhang mit der funktionalen Programmierung schief gehen?

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.

Grundsätzlich ist es schlecht, wenn es schlecht ist, unabhängig von OO oder funktionalem Programmierparadigma, oder?

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.

Doc Brown
quelle
4

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:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

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:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
Karl Bielefeldt
quelle
3

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:

  1. 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.

  2. 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.

  3. 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.

Superkatze
quelle
danke für die beeindruckende antwort !! Ich denke, die Ursache meiner Verwirrung liegt wahrscheinlich darin, dass ich aus dem C # -Hintergrund stamme und lerne, "funktionalen Stilcode in c # zu schreiben", der überall sagt, dass man veränderbare Objekte vermeiden soll - aber ich denke, dass Sprachen, die das Paradigma der funktionalen Programmierung umfassen, fördern (oder durchsetzen - nicht sicher sind wenn die Durchsetzung korrekt ist) Unveränderlichkeit.
rahulaga_dev
@RahulAgarwal: Es ist möglich, dass Verweise auf ein Objekt einen Wert kapseln , dessen Bedeutung nicht durch das Vorhandensein anderer Verweise auf dasselbe Objekt beeinflusst wird, eine Identität haben, die sie mit anderen Verweisen auf dasselbe Objekt verknüpft, oder keine. Wenn sich der Real-Word-Status ändert, kann entweder der Wert oder die Identität eines Objekts, das diesem Status zugeordnet ist, konstant sein, aber nicht beide - man muss sich ändern. Die 50.000 Dollar sollen was machen.
Supercat
1

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.

Aaron M. Eshbach
quelle