it-swarm.dev

Warum werden Nebenwirkungen in der funktionalen Programmierung als böse angesehen?

Ich halte Nebenwirkungen für ein natürliches Phänomen. Aber es ist so etwas wie ein Tabu in funktionalen Sprachen. Was sind die Gründe?

Meine Frage bezieht sich speziell auf den funktionalen Programmierstil. Nicht alle Programmiersprachen/Paradigmen.

70
Gulshan

Wenn Sie Ihre Funktionen/Methoden ohne Nebenwirkungen schreiben - es handelt sich also um reine Funktionen -, können Sie leichter über die Richtigkeit Ihres Programms nachdenken.

Es macht es auch einfach, diese Funktionen zusammenzustellen, um neues Verhalten zu erzeugen.

Es ermöglicht auch bestimmte Optimierungen, bei denen der Compiler beispielsweise die Ergebnisse von Funktionen auswendig lernen oder Common Subexpression Elimination verwenden kann.

Bearbeiten: auf Benjols Wunsch: Da ein Großteil Ihres Status im Stapel gespeichert ist (Datenfluss, nicht Kontrollfluss, wie Jonas es genannt hat hier ), können Sie die Ausführung dieser Teile parallelisieren oder auf andere Weise neu anordnen Ihrer Berechnung, die unabhängig voneinander sind. Sie können diese unabhängigen Teile leicht finden, da ein Teil keine Eingaben für den anderen liefert.

In Umgebungen mit Debuggern, in denen Sie den Stack zurücksetzen und das Rechnen fortsetzen können (wie Smalltalk), können Sie mit reinen Funktionen sehr leicht sehen, wie sich ein Wert ändert, da die vorherigen Status zur Überprüfung verfügbar sind. In einer mutationsintensiven Berechnung können Sie den Verlauf der Berechnung nicht sehen, es sei denn, Sie fügen Ihrer Struktur oder Ihrem Algorithmus explizit Do/Undo-Aktionen hinzu. (Dies knüpft an den ersten Absatz an: Das Schreiben reiner Funktionen erleichtert die Überprüfung der Richtigkeit Ihres Programms.)

73
Frank Shearar

Sie haben es falsch verstanden, funktionale Programmierung fördert die Begrenzung von Nebenwirkungen, um Programme leicht verständlich und optimierbar zu machen. Sogar mit Haskell können Sie in Dateien schreiben.

Im Wesentlichen sage ich, dass funktionale Programmierer Nebenwirkungen nicht für böse halten, sondern lediglich die Einschränkung der Verwendung von Nebenwirkungen für gut halten. Ich weiß, es mag wie eine so einfache Unterscheidung erscheinen, aber es macht den Unterschied.

24
ChaosPandion

Aus einem Artikel über Funktionale Programmierung :

In der Praxis müssen Anwendungen einige Nebenwirkungen haben. Simon Peyton-Jones, ein Hauptverantwortlicher für die funktionale Programmiersprache Haskell, sagte Folgendes: "Am Ende muss jedes Programm den Status manipulieren. Ein Programm, das keinerlei Nebenwirkungen hat, ist eine Art Black Box. Alles, was Sie sagen können, ist dass die Box heißer wird. " ( http://oscon.blip.tv/file/324976 ) Der Schlüssel besteht darin, Nebenwirkungen zu begrenzen, sie eindeutig zu identifizieren und zu vermeiden, dass sie im gesamten Code verteilt werden.

23
Peter Stuifzand

Ein paar Anmerkungen:

  • Funktionen ohne Nebenwirkungen können trivial parallel ausgeführt werden, während Funktionen mit Nebenwirkungen normalerweise eine Art Synchronisation erfordern.

  • Funktionen ohne Nebenwirkungen ermöglichen eine aggressivere Optimierung (z. B. durch transparente Verwendung eines Ergebniscaches), denn solange wir das richtige Ergebnis erhalten, spielt es keine Rolle, ob die Funktion wirklich war oder nicht hingerichtet

13
user281377

Ich arbeite jetzt hauptsächlich im Funktionscode, und aus dieser Perspektive scheint es offensichtlich. Nebenwirkungen verursachen eine enorme mentale Belastung für Programmierer, die versuchen, Code zu lesen und zu verstehen. Sie bemerken diese Belastung erst, wenn Sie eine Weile frei davon sind, und müssen dann plötzlich wieder Code mit Nebenwirkungen lesen.

Betrachten Sie dieses einfache Beispiel:

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

In einer funktionalen Sprache weiß ich , dass foo immer noch 42 ist. Ich muss nicht einmal Schauen Sie sich den Code dazwischen an , verstehen Sie ihn noch weniger, oder schauen Sie sich die Implementierungen der aufgerufenen Funktionen an.

Das ganze Zeug über Parallelität, Parallelisierung und Optimierung ist schön, aber genau das haben Informatiker in die Broschüre aufgenommen. Ich muss mich nicht fragen, wer Ihre Variable mutiert und wann es das ist, was ich im täglichen Üben wirklich genieße.

11
Karl Bielefeldt

Nur wenige bis keine Sprachen machen es unmöglich, Nebenwirkungen zu verursachen. Sprachen, die völlig nebenwirkungsfrei waren, wären nur in sehr begrenztem Umfang unerschwinglich schwierig (nahezu unmöglich) zu verwenden.

Warum werden Nebenwirkungen als böse angesehen?

Weil sie es viel schwieriger machen, genau zu überlegen, was ein Programm tut, und zu beweisen, dass es das tut, was Sie von ihm erwarten.

Stellen Sie sich vor, Sie testen auf einer sehr hohen Ebene eine gesamte dreistufige Website nur mit Black-Box-Tests. Sicher, es ist machbar, abhängig von der Skala. Aber es gibt sicherlich viele Überschneidungen. Und wenn is ein Fehler vorliegt (der mit einem Nebeneffekt zusammenhängt), können Sie möglicherweise das gesamte System für weitere Tests beschädigen, bis der Fehler diagnostiziert und behoben ist und der Fehler behoben ist in der Testumgebung bereitgestellt.

Vorteile

Jetzt verkleinere das. Wenn Sie ziemlich gut darin wären, nebenwirkungsfreien Code zu schreiben, wie viel schneller würden Sie darüber nachdenken, was ein vorhandener Code getan hat? Wie viel schneller könnten Sie Unit-Tests schreiben? Wie sicher würden Sie sich fühlen, dass der Code ohne Nebenwirkungen garantiert fehlerfrei war und dass Benutzer ihre Gefährdung durch Fehler begrenzen konnten, die did hatten?

Wenn Code keine Nebenwirkungen hat, kann der Compiler auch zusätzliche Optimierungen vornehmen, die er ausführen kann. Es kann viel einfacher sein, diese Optimierungen zu implementieren. Es ist möglicherweise viel einfacher, eine Optimierung für nebenwirkungsfreien Code zu konzipieren. Dies bedeutet, dass Ihr Compiler-Anbieter möglicherweise Optimierungen implementiert, die in Code mit Nebenwirkungen schwer bis unmöglich sind.

Parallelität ist auch drastisch einfacher zu implementieren, automatisch zu generieren und zu optimieren, wenn Code keine Nebenwirkungen hat. Dies liegt daran, dass alle Teile in beliebiger Reihenfolge sicher bewertet werden können. Es wird allgemein als die nächste große Herausforderung angesehen, der sich die Informatik stellen muss, Programmierern das gleichzeitige Schreiben von Code zu ermöglichen, und als eine der wenigen verbleibenden Absicherungen gegen Moores Gesetz .

Nebenwirkungen sind wie "Lecks" in Ihrem Code, die später entweder von Ihnen oder einem ahnungslosen Mitarbeiter behandelt werden müssen.

Funktionale Sprachen vermeiden Zustandsvariablen und veränderbare Daten, um Code weniger kontextabhängig und modularer zu gestalten. Die Modularität stellt sicher, dass die Arbeit eines Entwicklers die Arbeit eines anderen Entwicklers nicht beeinträchtigt/untergräbt.

Die Skalierung der Entwicklungsrate mit der Teamgröße ist heute ein "heiliger Gral" der Softwareentwicklung. Bei der Arbeit mit anderen Programmierern sind nur wenige Dinge so wichtig wie die Modularität. Selbst die einfachsten logischen Nebenwirkungen erschweren die Zusammenarbeit erheblich.

4
Ami

Nun, meiner Meinung nach ist das ziemlich scheinheilig. Niemand mag Nebenwirkungen, aber jeder braucht sie.

Was an Nebenwirkungen so gefährlich ist, ist, dass wenn Sie eine Funktion aufrufen, dies möglicherweise nicht nur Auswirkungen auf das Verhalten der Funktion beim nächsten Aufruf hat, sondern möglicherweise auch auf andere Funktionen. Nebenwirkungen führen daher zu unvorhersehbarem Verhalten und nicht trivialen Abhängigkeiten.

Programmierparadigmen wie OO und funktional adressieren beide dieses Problem. OO reduziert das Problem durch Auferlegung einer Trennung von Bedenken. Dies bedeutet den Anwendungsstatus, der aus besteht Viele veränderbare Daten werden in Objekte eingekapselt, von denen jedes nur für die Aufrechterhaltung seines eigenen Zustands verantwortlich ist. Auf diese Weise wird das Risiko von Abhängigkeiten verringert und Probleme sind weitaus isolierter und leichter zu verfolgen.

Die funktionale Programmierung verfolgt einen weitaus radikaleren Ansatz, bei dem der Anwendungsstatus aus Sicht des Programmierers einfach unveränderlich ist. Dies ist eine nette Idee, macht aber die Sprache für sich genommen unbrauchbar. Warum? Weil JEDE E/A-Operation Nebenwirkungen hat. Sobald Sie aus einem Eingabestream lesen, ändert sich wahrscheinlich Ihr Anwendungsstatus, da das Ergebnis beim nächsten Aufrufen derselben Funktion wahrscheinlich unterschiedlich ist. Möglicherweise lesen Sie andere Daten, oder - möglicherweise auch - der Vorgang schlägt fehl. Gleiches gilt für die Ausgabe. Sogar die Ausgabe ist eine Operation mit Nebenwirkungen. Dies ist nichts, was Sie heutzutage oft bemerken, aber stellen Sie sich vor, Sie haben nur 20.000 für Ihre Ausgabe und wenn Sie mehr ausgeben, stürzt Ihre App ab, weil Sie nicht mehr über genügend Speicherplatz verfügen oder was auch immer.

Also ja, Nebenwirkungen sind aus Sicht eines Programmierers böse und gefährlich. Die meisten Fehler entstehen durch die Art und Weise, wie bestimmte Teile des Anwendungsstatus durch unüberlegte und häufig unnötige Nebenwirkungen auf nahezu dunkle Weise miteinander verbunden werden. Aus der Sicht eines Benutzers sind Nebenwirkungen der Punkt, an dem ein Computer verwendet wird. Sie kümmern sich nicht darum, was im Inneren passiert oder wie es organisiert ist. Sie tun etwas und erwarten, dass sich der Computer entsprechend ändert.

4
back2dos

Jede Nebenwirkung führt zusätzliche Eingabe-/Ausgabeparameter ein, die beim Testen berücksichtigt werden müssen.

Dies macht die Codeüberprüfung viel komplexer, da die Umgebung nicht nur auf den zu validierenden Code beschränkt werden kann, sondern einen Teil oder die gesamte umgebende Umgebung einbeziehen muss (die aktualisierte globale Umgebung befindet sich in diesem Code dort drüben, was wiederum davon abhängt Code, der wiederum davon abhängt, in einem vollständigen Java EE-Server ....) zu leben.

Indem Sie versuchen, Nebenwirkungen zu vermeiden, begrenzen Sie den Umfang des Externalismus, der zum Ausführen des Codes erforderlich ist.

2
user1249

Nach meiner Erfahrung erfordert gutes Design in der objektorientierten Programmierung die Verwendung von Funktionen, die Nebenwirkungen haben.

Nehmen Sie zum Beispiel eine grundlegende UI-Desktopanwendung. Möglicherweise habe ich ein laufendes Programm, auf dessen Heap sich ein Objektdiagramm befindet, das den aktuellen Status des Domänenmodells meines Programms darstellt. Nachrichten kommen zu den Objekten in diesem Diagramm (z. B. über Methodenaufrufe, die vom UI-Layer-Controller aufgerufen werden). Das Objektdiagramm (Domänenmodell) auf dem Heap wird als Reaktion auf die Nachrichten geändert. Beobachter des Modells werden über Änderungen informiert, die Benutzeroberfläche und möglicherweise andere Ressourcen werden geändert.

Weit davon entfernt, böse zu sein, ist die korrekte Anordnung dieser haufenmodifizierenden und bildschirmmodifizierenden Nebenwirkungen das Kernstück von OO Design (in diesem Fall das MVC-Muster).

Das bedeutet natürlich nicht, dass Ihre Methoden willkürliche Nebenwirkungen haben sollten. Und nebenwirkungsfreie Funktionen können die Lesbarkeit und manchmal die Leistung Ihres Codes verbessern.

1
flamingpenguin

Wie die obigen Fragen gezeigt haben, verhindern funktionale Sprachen nicht so sehr , dass Code Nebenwirkungen hat, sondern bieten uns Tools zum Verwalten der möglichen Nebenwirkungen passieren in einem bestimmten Code und wann.

Dies hat sehr interessante Konsequenzen. Erstens und am offensichtlichsten gibt es zahlreiche Dinge, die Sie mit nebenwirkungsfreiem Code tun können, die bereits beschrieben wurden. Aber es gibt noch andere Dinge, die wir tun können, selbst wenn wir mit Code arbeiten, der Nebenwirkungen hat:

  • In Code mit veränderlichem Status können wir den Umfang des Status so verwalten, dass statisch sichergestellt wird, dass er nicht außerhalb einer bestimmten Funktion ausläuft. Auf diese Weise können wir Müll ohne Referenzzähl- oder Mark-and-Sweep-Schemata sammeln und dennoch sicher sein, dass keine Referenzen erhalten bleiben. Dieselben Garantien sind auch nützlich für die Pflege datenschutzrelevanter Informationen usw. (Dies kann mit der ST-Monade in haskell erreicht werden.)
  • Wenn Sie den gemeinsam genutzten Status in mehreren Threads ändern, können Sie die Notwendigkeit von Sperren vermeiden, indem Sie Änderungen verfolgen und am Ende einer Transaktion eine atomare Aktualisierung durchführen oder die Transaktion zurücksetzen und wiederholen, wenn ein anderer Thread eine widersprüchliche Änderung vorgenommen hat. Dies ist nur erreichbar, weil wir sicherstellen können, dass der Code keine anderen Auswirkungen als die Statusänderungen hat (die wir gerne aufgeben können). Dies wird von der STM-Monade (Software Transactional Memory) in Haskell durchgeführt.
  • wir können die Auswirkungen von Code verfolgen und ihn trivial schleifen, indem wir alle Effekte filtern, die möglicherweise ausgeführt werden müssen, um sicherzustellen, dass er sicher ist, und so (zum Beispiel) vom Benutzer eingegebener Code kann sicher auf einer Website ausgeführt werden)
0
Jules

Das Böse ist etwas übertrieben. Es hängt alles vom Kontext des Sprachgebrauchs ab.

Eine weitere Überlegung zu den bereits erwähnten ist, dass es Beweise für die Richtigkeit eines Programms viel einfacher macht, wenn es keine funktionellen Nebenwirkungen gibt.

0
Ilan

In komplexen Codebasen sind komplexe Wechselwirkungen von Nebenwirkungen das Schwierigste, worüber ich nachdenken kann. Ich kann nur persönlich sprechen, wenn mein Gehirn so funktioniert. Nebenwirkungen und anhaltende Zustände und mutierende Eingaben usw. lassen mich darüber nachdenken, wann und wo Dinge über die Korrektheit begründen, nicht nur darüber, was in jeder einzelnen Funktion geschieht.

Ich kann mich nicht nur auf "was" konzentrieren. Ich kann nach gründlichem Testen einer Funktion, die Nebenwirkungen verursacht, nicht den Schluss ziehen, dass sie einen zuverlässigen Eindruck im gesamten Code verbreitet, da Anrufer sie möglicherweise immer noch missbrauchen, indem sie sie zur falschen Zeit, vom falschen Thread im falschen Thread aufrufen Auftrag. In der Zwischenzeit ist es so gut wie unmöglich, eine Funktion, die keine Nebenwirkungen verursacht und bei einer Eingabe nur eine neue Ausgabe zurückgibt (ohne die Eingabe zu berühren), auf diese Weise zu missbrauchen.

Aber ich bin ein pragmatischer Typ, denke ich, oder versuche es zumindest, und ich denke nicht, dass wir alle Nebenwirkungen auf ein Minimum reduzieren müssen, um über die Richtigkeit unseres Codes nachzudenken (zumindest) Ich würde es sehr schwierig finden, dies in Sprachen wie C) zu tun. Ich finde es sehr schwierig, über Korrektheit nachzudenken, wenn wir die Kombination aus komplexen Kontrollabläufen und Nebenwirkungen haben.

Komplexe Kontrollflüsse sind für mich solche, die grafischer Natur sind, oft rekursiv oder rekursiv (Ereigniswarteschlangen, z. B. die Ereignisse nicht direkt rekursiv aufrufen, sondern "rekursiv" sind) und möglicherweise Dinge tun Beim Durchlaufen einer tatsächlichen verknüpften Diagrammstruktur oder beim Verarbeiten einer inhomogenen Ereigniswarteschlange, die eine eklektische Mischung von zu verarbeitenden Ereignissen enthält, die uns zu allen möglichen Teilen der Codebasis führt und alle unterschiedliche Nebenwirkungen auslöst. Wenn Sie versuchen würden, alle Stellen zu zeichnen, an denen Sie letztendlich im Code landen, würde dies einem komplexen Diagramm ähneln und möglicherweise mit Knoten im Diagramm, von denen Sie nie erwartet hätten, dass sie in diesem bestimmten Moment vorhanden gewesen wären, und vorausgesetzt, dass sie alle vorhanden sind Dies bedeutet, dass Sie möglicherweise nicht nur überrascht sind, welche Funktionen aufgerufen werden, sondern auch, welche Nebenwirkungen während dieser Zeit auftreten und in welcher Reihenfolge sie auftreten.

Funktionale Sprachen können äußerst komplexe und rekursive Kontrollflüsse aufweisen, aber das Ergebnis ist in Bezug auf die Korrektheit so einfach zu verstehen, da dabei nicht alle möglichen eklektischen Nebenwirkungen auftreten. Nur wenn komplexe Kontrollflüsse auf eklektische Nebenwirkungen treffen, ist es für mich kopfschmerzauslösend, zu versuchen, die Gesamtheit der Vorgänge zu verstehen und festzustellen, ob sie immer das Richtige tun.

Wenn ich diese Fälle habe, finde ich es oft sehr schwierig, wenn nicht unmöglich, mich über die Richtigkeit eines solchen Codes sehr sicher zu fühlen, geschweige denn sehr sicher, dass ich Änderungen an diesem Code vornehmen kann, ohne auf etwas Unerwartetes zu stoßen. Die Lösung für mich besteht darin, entweder den Kontrollfluss zu vereinfachen oder die Nebenwirkungen zu minimieren/zu vereinheitlichen (mit vereinheitlichen meine ich, dass in einer bestimmten Phase des Systems nur eine Art von Nebenwirkung für viele Dinge verursacht wird, nicht zwei oder drei oder eine Dutzend). Ich brauche eines dieser beiden Dinge, damit mein einfaches Gehirn sich über die Richtigkeit des vorhandenen Codes und die Richtigkeit der von mir eingeführten Änderungen sicher fühlen kann. Es ist ziemlich einfach, sich über die Richtigkeit des Codes, der Nebenwirkungen einführt, sicher zu sein, wenn die Nebenwirkungen zusammen mit dem Kontrollfluss einheitlich und einfach sind, wie folgt:

for each pixel in an image:
    make it red

Es ist ziemlich einfach, über die Richtigkeit eines solchen Codes nachzudenken, aber hauptsächlich, weil die Nebenwirkungen so gleichmäßig sind und der Kontrollfluss so einfach ist. Angenommen, wir hatten folgenden Code:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove Edge
         remove vertex

Dann ist dies ein lächerlich stark vereinfachter Pseudocode, der typischerweise viel mehr Funktionen und verschachtelte Schleifen und viel mehr Dinge beinhaltet, die weitergehen müssten (Aktualisieren mehrerer Texturkarten, Knochengewichte, Auswahlzustände usw.), aber selbst der Pseudocode macht es so schwierig Grund für die Korrektheit aufgrund des Zusammenspiels des komplexen graphischen Kontrollflusses und der auftretenden Nebenwirkungen. Eine Strategie zur Vereinfachung besteht darin, die Verarbeitung zu verschieben und sich jeweils nur auf eine Art von Nebenwirkung zu konzentrieren:

for each vertex to remove:
     mark connected edges
for each marked Edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked Edge:
     remove Edge
for each vertex to remove:
     remove vertex

... etwas in diesem Sinne als eine Iteration der Vereinfachung. Das bedeutet, dass wir die Daten mehrmals durchlaufen, was definitiv einen Rechenaufwand verursacht. Wir stellen jedoch häufig fest, dass wir diesen resultierenden Code leichter multithreaden können, da die Nebenwirkungen und Kontrollflüsse diese einheitliche und einfachere Natur angenommen haben. Darüber hinaus kann jede Schleife cachefreundlicher gestaltet werden, als den verbundenen Graphen zu durchlaufen und dabei Nebenwirkungen zu verursachen (z. B. verwenden Sie einen parallelen Bit-Satz, um zu markieren, was durchlaufen werden muss, damit wir die verzögerten Durchläufe in sortierter Reihenfolge ausführen können mit Bitmasken und FFS). Vor allem aber finde ich es viel einfacher, über die zweite Version nachzudenken, wenn es um Korrektheit und Änderungen geht, ohne Fehler zu verursachen. So gehe ich es trotzdem an und wende die gleiche Denkweise an, um die Netzverarbeitung dort oben zu vereinfachen wie die Vereinfachung der Ereignisbehandlung usw. - homogenere Schleifen mit toten einfachen Kontrollabläufen, die einheitliche Nebenwirkungen verursachen.

Und schließlich müssen irgendwann Nebenwirkungen auftreten, sonst hätten wir nur Funktionen, die Daten ausgeben, ohne dass es irgendwohin geht. Oft müssen wir etwas in eine Datei aufnehmen, etwas auf einem Bildschirm anzeigen, die Daten über einen Socket senden, etwas in dieser Art, und all diese Dinge sind Nebenwirkungen. Aber wir können definitiv die Anzahl überflüssiger Nebenwirkungen reduzieren, die auftreten, und auch die Anzahl der Nebenwirkungen, die auftreten, wenn die Kontrollflüsse sehr kompliziert sind, und ich denke, es wäre viel einfacher, Fehler zu vermeiden, wenn wir dies tun würden.

0
user204677