Werden in der funktionalen Programmierung lokale veränderbare Variablen ohne Nebenwirkungen immer noch als „schlechte Praxis“ betrachtet?

23

Werden veränderbare lokale Variablen in einer Funktion, die nur intern verwendet werden (z. B. die Funktion hat keine Nebenwirkungen, zumindest nicht absichtlich), immer noch als "nicht funktionsfähig" betrachtet?

zB bei der "Funktionsprogrammierung mit Scala" wird bei der Prüfung des Kursstils jede varVerwendung als schlecht eingestuft

Meine Frage, ob die Funktion keine Nebenwirkungen hat, ist das Schreiben von imperativem Stilcode immer noch nicht empfehlenswert?

Was ist z. B. falsch daran, eine lokale for-Schleife zu erstellen, eine lokale Mutable zu erstellen ListBufferund zu addieren, anstatt die Tail-Rekursion mit dem Akkumulatormuster zu verwenden, solange die Eingabe nicht geändert wird?

Wenn die Antwort "Ja, sie sind immer entmutigt, auch wenn es keine Nebenwirkungen gibt", was ist dann der Grund?

Eran Medan
quelle
3
Alle Ratschläge, Ermahnungen usw. zu dem Thema, die ich jemals gehört habe, beziehen sich auf den geteilten veränderlichen Zustand als Quelle der Komplexität. Ist dieser Kurs nur für Anfänger gedacht? Dann ist es wahrscheinlich eine gut gemeinte absichtliche Vereinfachung.
Kilian Foth
3
@KilianFoth: Der gemeinsam genutzte veränderbare Status ist ein Problem in Multithread-Kontexten, aber der nicht gemeinsam genutzte veränderbare Status kann dazu führen, dass es auch schwierig ist, über Programme nachzudenken.
Michael Shaw
1
Ich denke, die Verwendung einer lokalen veränderlichen Variablen ist nicht unbedingt eine schlechte Übung, aber kein "funktionaler Stil": Ich denke, der Zweck des Scala-Kurses (den ich im letzten Herbst belegt habe) besteht darin, Ihnen das Programmieren in einem funktionalen Stil beizubringen. Sobald Sie klar zwischen funktionalem und imperativem Stil unterscheiden können, können Sie entscheiden, wann Sie welchen verwenden möchten (sofern Ihre Programmiersprache beides zulässt). varist immer nicht funktionsfähig. Scala verfügt über eine Lazy Vals- und Schwanzrekursionsoptimierung, die es ermöglicht, Vars vollständig zu vermeiden.
Giorgio

Antworten:

17

Das Einzige, was hier eindeutig schlecht ist, ist die Behauptung, dass etwas eine reine Funktion ist, wenn es nicht so ist.

Wenn veränderbare Variablen in einer Weise verwendet werden, die wirklich und vollständig in sich geschlossen ist, ist die Funktion äußerlich rein und jeder ist glücklich. Haskell unterstützt dies ausdrücklich , wobei das Typsystem sogar sicherstellt, dass veränderbare Referenzen nicht außerhalb der Funktion verwendet werden können, die sie erstellt.

Trotzdem denke ich, dass das Sprechen über "Nebenwirkungen" nicht die beste Art ist, es zu betrachten (und deswegen habe ich oben "rein" gesagt). Alles, was eine Abhängigkeit zwischen der Funktion und dem externen Zustand erzeugt, erschwert das Nachdenken und beinhaltet Dinge wie die Kenntnis der aktuellen Zeit oder die Verwendung des verdeckten veränderlichen Zustands auf nicht thread-sichere Weise.

CA McCann
quelle
16

Das Problem ist nicht die Veränderbarkeit an sich, es ist ein Mangel an referentieller Transparenz.

Eine referenziell transparente Sache und eine Referenz darauf müssen immer gleich sein, so dass eine referenziell transparente Funktion für eine gegebene Menge von Eingaben immer die gleichen Ergebnisse zurückgibt und eine referenziell transparente "Variable" wirklich ein Wert und keine Variable ist, da sie kann mich nicht ändern. Sie können eine referenziell transparente Funktion erstellen, die eine veränderbare Variable enthält. das ist kein problem Es ist möglicherweise schwieriger zu gewährleisten, dass die Funktion referenziell transparent ist, je nachdem, was Sie tun.

Ich kann mir nur einen Fall vorstellen, bei dem die Veränderbarkeit genutzt werden muss, um etwas sehr Funktionsfähiges zu erreichen: das Auswendiglernen. Beim Speichern werden Werte aus einer Funktion zwischengespeichert, sodass sie nicht neu berechnet werden müssen. es ist referenziell transparent, aber es verwendet Mutation.

Aber im Allgemeinen gehören referenzielle Transparenz und Unveränderlichkeit zusammen, abgesehen von einer lokal veränderlichen Variablen in einer referenziell transparenten Funktion und Memoisierung. Ich bin mir nicht sicher, ob es andere Beispiele gibt, bei denen dies nicht der Fall ist.

Michael Shaw
quelle
4
Ihr Standpunkt zum Auswendiglernen ist sehr gut. Beachten Sie, dass Haskell die referenzielle Transparenz für die Programmierung stark betont, das memo-ähnliche Verhalten der verzögerten Auswertung jedoch eine erstaunliche Menge an Mutationen beinhaltet, die von der Sprachlaufzeit hinter den Kulissen vorgenommen werden.
CA McCann
@CA McCann: Ich denke, was Sie sagen, ist sehr wichtig: In einer funktionalen Sprache kann die Laufzeit Mutation verwenden, um die Berechnung zu optimieren, aber es gibt kein Konstrukt in der Sprache, das es dem Programmierer ermöglicht, Mutation zu verwenden. Ein weiteres Beispiel ist eine while-Schleife mit einer Schleifenvariablen: In Haskell können Sie eine rekursive Tail-Funktion schreiben, die möglicherweise mit einer veränderlichen Variablen implementiert wird (um die Verwendung des Stacks zu vermeiden). Der Programmierer sieht jedoch unveränderliche Funktionsargumente, die von einer Variablen übergeben werden ruf zum nächsten.
Giorgio
@Michael Shaw: +1 für "Das Problem ist nicht an sich veränderlich, es ist ein Mangel an referentieller Transparenz." Vielleicht können Sie die Clean-Sprache zitieren, in der Sie Eindeutigkeitstypen haben: Diese ermöglichen die Veränderbarkeit, garantieren aber dennoch referenzielle Transparenz.
Giorgio
@Giorgio: Ich weiß wirklich nichts über Clean, obwohl ich es von Zeit zu Zeit erwähnt habe. Vielleicht sollte ich mich darum kümmern.
Michael Shaw
@Michael Shaw: Ich weiß nicht viel über Clean, aber ich weiß, dass es Eindeutigkeitstypen verwendet, um referenzielle Transparenz zu gewährleisten. Grundsätzlich können Sie ein Datenobjekt ändern, sofern Sie nach der Änderung keine Verweise auf den alten Wert haben. IMO, dies verdeutlicht Ihren Standpunkt: Referenzielle Transparenz ist der wichtigste Punkt, und Unveränderlichkeit ist nur eine Möglichkeit, dies sicherzustellen.
Giorgio
8

Es ist nicht wirklich gut, dies auf "gute Praxis" oder "schlechte Praxis" zu reduzieren. Scala unterstützt veränderbare Werte, weil sie bestimmte Probleme viel besser lösen als unveränderbare Werte, nämlich solche, die iterativer Natur sind.

Für die Perspektive bin ich mir ziemlich sicher, dass über CanBuildFromfast alle unveränderlichen Strukturen, die von scala bereitgestellt werden, eine Art von interner Mutation erfolgt. Der Punkt ist, dass das, was sie aussetzen, unveränderlich ist. Indem Sie so viele Werte wie möglich unveränderlich halten, können Sie leichter über das Programm nachdenken und sind weniger fehleranfällig .

Dies bedeutet nicht, dass Sie veränderbare Strukturen und Werte intern unbedingt vermeiden müssen, wenn Sie ein Problem haben, das sich besser für die Veränderbarkeit eignet.

In Anbetracht dessen können viele Probleme, die normalerweise veränderbare Variablen erfordern (wie z. B. Schleifen), mit vielen Funktionen höherer Ordnung, die Sprachen wie Scala bereitstellen (map / filter / fold), besser gelöst werden. Sei dir dessen bewusst.

KChaloux
quelle
2
Ja, ich brauche fast nie eine for-Schleife, wenn ich Scalas Sammlungen verwende. map, filter, foldLeftUnd forEach der Trick die meiste Zeit, aber wenn sie es nicht tun, fühlen sich in der Lage, I „OK“ , um Rückkehr zu Brute - Force - Imperativ Code bin , ist schön. (solange es keine Nebenwirkungen gibt natürlich)
Eran Medan
3

Abgesehen von möglichen Problemen mit der Threadsicherheit verlieren Sie normalerweise auch viel Typensicherheit. Imperative Schleifen haben den Rückgabetyp Unitund können so ziemlich jeden Ausdruck für Eingaben annehmen. Funktionen höherer Ordnung und sogar Rekursion weisen eine viel genauere Semantik und genauere Typen auf.

Sie haben auch viel mehr Optionen für die funktionale Containerverarbeitung als bei imperativen Schleifen. Mit zwingend notwendig, haben Sie grundsätzlich for, whileund kleinere Variationen dieser beiden wie do...whileund foreach.

Im funktionalen Bereich haben Sie Aggregat, Zählen, Filtern, Finden, FlatMap, Falten, GroupBy, LastIndexWhere, Map, MaxBy, MinBy, Partition, Scan, SortBy, SortWith, Span und TakeWhile, um nur einige weitere gebräuchliche von Scala zu nennen Standardbibliothek. Wenn Sie sich daran gewöhnt haben, diese zur Verfügung zu haben, erscheinen imperative forSchleifen im Vergleich zu grundlegend.

Der einzige wirkliche Grund, die lokale Veränderlichkeit zu nutzen, ist gelegentlich die Leistung.

Karl Bielefeldt
quelle
2

Ich würde sagen, dass es größtenteils in Ordnung ist. Darüber hinaus kann das Generieren von Strukturen in einigen Fällen eine gute Möglichkeit sein, die Leistung zu verbessern. Clojure hat dieses Problem durch die Bereitstellung transienter Datenstrukturen gelöst .

Die Grundidee besteht darin, lokale Mutationen in einem begrenzten Bereich zuzulassen und die Struktur dann einzufrieren, bevor sie zurückgegeben wird. Auf diese Weise kann Ihr Benutzer Ihren Code immer noch als reinen Code betrachten, aber Sie können bei Bedarf Transformationen an Ort und Stelle durchführen.

Wie der Link sagt:

Wenn ein Baum in den Wald fällt, macht es ein Geräusch? Wenn eine reine Funktion einige lokale Daten mutiert, um einen unveränderlichen Rückgabewert zu erzeugen, ist das in Ordnung?

Simon Bergot
quelle
2

Keine lokalen veränderbaren Variablen zu haben, hat einen Vorteil - es macht die Funktion gegenüber Threads freundlicher.

Ich habe mich durch eine solche lokale Variable verbrannt (weder in meinem Code noch in der Quelle), was zu einer Datenbeschädigung mit geringer Wahrscheinlichkeit geführt hat. Thread-Sicherheit wurde auf die eine oder andere Weise nicht erwähnt, es gab keinen Zustand, der über Anrufe hinweg bestand, und es gab keine Nebenwirkungen. Es ist mir nicht in den Sinn gekommen, dass es möglicherweise nicht threadsicher ist. Die Jagd nach einer 1 in 100.000 zufälligen Datenkorruption ist ein königlicher Schmerz.

Loren Pechtel
quelle