Funktionen, die für Anrufer rein aussehen, aber intern Mutationen verwenden

8

Ich habe gerade meine Kopie von Expert F # 2.0 bekommen und bin auf diese Aussage gestoßen, die mich etwas überrascht hat:

  

Zum Beispiel, wenn nötig, können Sie   Verwenden Sie Nebenwirkungen auf private Daten   Strukturen, die zu Beginn von   ein Algorithmus und dann diese verwerfen   Datenstrukturen vor der Rückgabe eines   Ergebnis; das Gesamtergebnis ist dann   effektiv ein Nebenwirkungsfreies   Funktion. Ein Beispiel für die Trennung   Von der F # -Bibliothek ist die Bibliothek   Implementierung von List.map, die verwendet   Mutation intern; die Schreibvorgänge treten auf   auf einem internen, getrennten Daten   Struktur, die kein anderer Code kann   Zugang.

Der Vorteil dieses Ansatzes liegt offensichtlich in der Leistung. Ich bin nur neugierig, ob es irgendwelche Nachteile gibt - gibt es hier irgendwelche Fallstricke, die mit Nebenwirkungen einhergehen können? Ist die Parallelität beeinträchtigt?

Mit anderen Worten, wenn die Leistung beiseite gelegt würde, wäre es vorzuziehen, List.map auf eine reine Weise zu implementieren?

(Dies betrifft natürlich vor allem F #, aber ich bin auch neugierig auf die allgemeine Philosophie)

    
J Cooper 13.09.2010, 03:53
quelle

7 Antworten

14

Ich denke, dass fast jeder Nachteil von Nebenwirkungen mit "Interaktion mit anderen Teilen des Programms" verbunden ist. Die Nebenwirkungen selbst sind nicht schlecht (wie @Gabe sagt, sogar ein reines funktionales Programm mutiert RAM ständig), es sind die gemeinsamen Folgen von Effekten (nicht lokale Interaktionen), die Probleme verursachen (mit Debugging / Leistung / Verständlichkeit) /etc.). So sind Effekte auf den rein lokalen Zustand (z. B. auf eine lokale Variable, die nicht entweicht) in Ordnung.

(Der einzige Nachteil, den ich mir vorstellen kann, ist, dass wenn ein Mensch solch ein lokales veränderliches Wesen sieht, müssen sie darüber nachdenken, ob es entkommen kann. In F # können lokale Mutables niemals entkommen (Verschlüsse können keine Mutables aufnehmen), so nur potentielle "mentale Steuer" kommt von Überlegungen über veränderbare Referenztypen.)

Zusammenfassung: Es ist in Ordnung, Effekte zu verwenden, solange es einfach ist, sich davon zu überzeugen, dass die Effekte nur auf nicht-flüchtige Einheimische wirken. (Es ist auch ok, Effekte in anderen Fällen zu verwenden, aber ich ignoriere diese anderen Fälle, da wir in diesem Frage-Thread die erleuchteten funktionalen Programmierer sind, die versuchen, Effekte zu meiden, wann immer es vernünftig ist. :))

(Wenn Sie sehr tief gehen wollen, sind lokale Effekte, wie die bei der Implementierung von F # s List.map, nicht nur ein Hindernis für Parallelisierbarkeit, sondern tatsächlich ein Vorteil, aus dem Gesichtspunkt, dass die mehr Eine effiziente Implementierung weist weniger Ressourcen zu und belastet somit weniger die gemeinsame Ressource des GC.)

    
Brian 13.09.2010, 05:50
quelle
6

Sie könnten an Simon Peyton Jones interessiert sein "Lazy Functional State Threads" . Ich habe es nur durch die ersten paar Seiten geschafft, die sehr klar sind (ich bin mir sicher, dass der Rest auch sehr klar ist).

Der wichtige Punkt ist, dass, wenn Sie Control.Monad.ST verwenden, um so etwas in Haskell zu machen, das Typsystem selbst die Kapselung erzwingt. In Scala (und wahrscheinlich in F #) ist der Ansatz mehr "vertraue uns einfach, dass wir mit diesem ListBuffer in deinem map nicht irgendetwas hinterhältig machen."

    
Travis Brown 13.09.2010 04:47
quelle
4

Wenn eine Funktion lokale, private (zur Funktion) veränderbare Datenstrukturen verwendet, ist die Parallelisierung nicht betroffen. Wenn also die Funktion map intern ein Array mit der Größe der Liste erstellt und iteriert über die Elemente, die das Array ausfüllen, können Sie map 100 Mal gleichzeitig in derselben Liste ausführen und sich keine Sorgen wegen jeder Instanz von% co_de machen % hat ein eigenes privates Array. Da Ihr Code den Inhalt des Arrays erst anzeigen kann, wenn er ausgefüllt wurde, ist er effektiv rein (denken Sie daran, dass Ihr Computer den Status des RAMs auf einer bestimmten Ebene tatsächlich ändern muss).

Wenn andererseits eine Funktion globale veränderbare Datenstrukturen verwendet, könnte die Parallelisierung beeinträchtigt werden. Angenommen, Sie haben eine Funktion map . Offensichtlich liegt der Sinn darin, einen globalen Zustand zu erhalten (obwohl "global" in dem Sinne, dass es nicht lokal für den Funktionsaufruf ist, ist es immer noch "privat" in dem Sinne, dass es außerhalb der Funktion nicht zugänglich ist) es muss keine Funktion mehrere Male mit den gleichen Argumenten ausführen, aber es ist immer noch rein, weil die gleichen Eingaben immer die gleichen Ausgaben erzeugen. Wenn Ihre Cache-Datenstruktur Thread-sicher ist (wie Memoize ), können Sie Ihre Funktion trotzdem parallel mit sich selbst ausführen. Wenn nicht, dann könnten Sie argumentieren, dass die Funktion nicht rein ist, weil sie Nebenwirkungen hat, die beobachtbar sind, wenn sie gleichzeitig ausgeführt werden.

Ich sollte hinzufügen, dass es eine übliche Technik in F # ist, mit einer rein funktionalen Routine zu beginnen und sie dann zu optimieren, indem man den veränderlichen Zustand nutzt (zB Caching, explizites Looping), wenn das Profiling zeigt, dass es zu langsam ist.

>     
Gabe 13.09.2010 04:48
quelle
3

Derselbe Ansatz kann in Clojure verwendet werden. Die unveränderlichen Datenstrukturen in Clojure - Liste, Karte und Vektor - haben ihre "vorübergehenden" Gegenstücke, die veränderbar sind. Die Clojure-Referenz zu Transient drängt darauf, sie nur in dem Code zu verwenden, der von "keinem anderen Code" gesehen werden kann.

Im Client-Code gibt es Wächter gegen undichte Transienten:

  • Die üblichen Funktionen, die an den unveränderlichen Datenstrukturen arbeiten, funktionieren nicht bei Transienten. Wenn sie aufgerufen werden, wird eine Ausnahme ausgelöst.

  • Die Transienten sind an den Thread gebunden, in dem sie erstellt wurden. Wenn sie von einem anderen Thread geändert werden, wird eine Ausnahme ausgelöst.

Der clojure.core-Code selbst verwendet eine Menge Transienten hinter den Kulissen.

Der Hauptvorteil der Verwendung von Transienten ist die enorme Beschleunigung, die sie bieten.

So scheint der streng kontrollierte Gebrauch des veränderlichen Zustands in den funktionalen Sprachen OK zu sein.

    
Abhinav Sarkar 13.09.2010 05:24
quelle
2

Es hat keinen Einfluss darauf, ob die Funktion parallel zu anderen Funktionen ausgeführt werden kann. Es wirkt sich darauf aus, ob die internen Funktionen der Funktion parallel ausgeführt werden können - aber das ist wahrscheinlich kein Problem für die meisten kleinen Funktionen (wie z. B. Karten), die auf einen PC zielen.

Ich habe bemerkt, dass einige gute F # -Programmierer (im Internet und in Büchern) sehr entspannt darüber sind, imperative Techniken für Loops zu verwenden. Sie scheinen eine einfache Schleife mit veränderbaren Schleifenvariablen einer komplexen rekursiven Funktion vorzuziehen.

    
Stephen Hosking 13.09.2010 04:27
quelle
2

Ein Problem kann sein, dass ein guter funktionaler Compiler konstruiert wird, um "funktionalen" Code gut zu optimieren, aber wenn Sie etwas veränderbares Zeug verwenden, kann der Compiler nicht so gut wie im anderen Fall optimieren. Im schlimmsten Fall führt dies zu ineffizienteren Algorithmen als die unveränderliche Variante.

Das andere Thema, an das ich denken kann, ist Faulheit - eine veränderliche Datenstruktur ist normalerweise nicht faul, daher kann eine veränderliche Funktion unnötige Bewertung von Argumenten erzwingen.

    
fuz 13.09.2010 05:47
quelle
0

Ich würde dies mit einer Frage beantworten: "Schreiben Sie die Funktion oder verwenden Sie die Funktion?"

Es gibt zwei Aspekte für Funktionen, Benutzer und Entwickler.

Als Benutzer interessiert man sich überhaupt nicht für die interne Struktur einer Funktion. Es kann in Byte-Code codiert werden und harte Nebeneffekte von jetzt bis zum Bewertungstag verwenden, solange es dem Vertrag von Daten entspricht, die Daten, die man erwartet, übereinstimmen. Eine Funktion ist eine Blackbox oder ein Orakel, ihre interne Struktur ist irrelevant (vorausgesetzt, sie macht nichts Dummes und Äußeres).

Als Entwickler einer Funktion ist die interne Struktur sehr wichtig. Unveränderlichkeit, konstante Korrektheit und Vermeidung von Nebenwirkungen helfen alle, eine Funktion zu entwickeln und aufrechtzuerhalten und die Funktion in die parallele Domäne zu erweitern.

Viele Menschen entwickeln eine Funktion, die sie dann verwenden, also gelten beide Aspekte.

Was die Vorteile von Unveränderlichkeit gegenüber veränderlichen Strukturen sind, ist eine andere Frage.

    
Snark 13.09.2010 19:40
quelle