Vorteile der zustandslosen Programmierung?

131

Ich habe kürzlich etwas über funktionale Programmierung gelernt (speziell Haskell, aber ich habe auch Tutorials zu Lisp und Erlang durchgearbeitet). Obwohl ich die Konzepte sehr aufschlussreich fand, sehe ich immer noch nicht die praktische Seite des Konzepts "keine Nebenwirkungen". Was sind die praktischen Vorteile davon? Ich versuche, in der funktionalen Denkweise zu denken, aber es gibt Situationen, die einfach zu komplex erscheinen, ohne dass der Zustand auf einfache Weise gespeichert werden kann (ich halte Haskells Monaden nicht für "einfach").

Lohnt es sich, Haskell (oder eine andere rein funktionale Sprache) weiter gründlich zu lernen? Ist funktionale oder zustandslose Programmierung tatsächlich produktiver als prozedurale? Ist es wahrscheinlich, dass ich Haskell oder eine andere funktionale Sprache später weiterhin verwenden werde, oder sollte ich sie nur zum Verständnis lernen?

Leistung ist mir weniger wichtig als Produktivität. Ich frage mich also hauptsächlich, ob ich in einer funktionalen Sprache produktiver sein werde als in einer prozeduralen / objektorientierten / was auch immer.

Sasha Chedygov
quelle

Antworten:

167

Lesen Sie die funktionale Programmierung auf den Punkt .

Die zustandslose Programmierung bietet viele Vorteile, darunter nicht zuletzt dramatisch Multithread-Code und gleichzeitiger Code. Um es ganz klar auszudrücken, der veränderliche Zustand ist der Feind des Multithread-Codes. Wenn die Werte standardmäßig unveränderlich sind, müssen sich Programmierer nicht darum kümmern, dass ein Thread den Wert des gemeinsam genutzten Status zwischen zwei Threads ändert, sodass eine ganze Klasse von Multithreading-Fehlern im Zusammenhang mit den Race-Bedingungen beseitigt wird. Da es keine Rennbedingungen gibt, gibt es auch keinen Grund, Sperren zu verwenden, sodass die Unveränderlichkeit eine weitere ganze Klasse von Fehlern im Zusammenhang mit Deadlocks beseitigt.

Das ist der große Grund, warum funktionale Programmierung wichtig ist, und wahrscheinlich der beste, um in den Zug der funktionalen Programmierung einzusteigen. Es gibt auch viele andere Vorteile, einschließlich vereinfachtem Debugging (dh Funktionen sind rein und mutieren nicht in anderen Teilen einer Anwendung), knapperem und ausdrucksstärkerem Code, weniger Boilerplate-Code im Vergleich zu Sprachen, die stark von Entwurfsmustern abhängen, und Der Compiler kann Ihren Code aggressiver optimieren.

Julia
quelle
5
Ich unterstütze das! Ich glaube, dass funktionale Programmierung in Zukunft aufgrund ihrer Eignung für parallele Programmierung viel häufiger eingesetzt wird.
Ray Hidayat
@ Ray: Ich würde auch verteilte Programmierung hinzufügen!
Anton Tykhyy
Das meiste davon ist wahr, außer für das Debuggen. In Haskell ist es im Allgemeinen schwieriger, weil Sie keinen echten Aufrufstapel haben, sondern nur einen Musterübereinstimmungsstapel. Und es ist viel schwieriger vorherzusagen, wie Ihr Code endet.
Hasufell
3
Außerdem geht es bei der funktionalen Programmierung nicht wirklich um "zustandslos". Rekursion ist bereits ein impliziter (lokaler) Zustand und die Hauptsache, die wir in haskell tun. Dies wird deutlich, wenn Sie einige nicht triviale Algorithmen in idiomatischem Haskell implementieren (z. B. rechnergestützte Geometrie) und Spaß beim Debuggen haben.
Hasufell
2
Ich mag es nicht, Staatenlose mit FP gleichzusetzen. Viele FP-Programme sind mit Status gefüllt, es existiert einfach in einem Abschluss und nicht in einem Objekt.
Mikemaccana
46

Je mehr Teile Ihres Programms zustandslos sind, desto mehr Möglichkeiten gibt es, Teile zusammenzusetzen, ohne dass etwas kaputt geht . Die Kraft des staatenlosen Paradigmas liegt nicht in der Staatenlosigkeit (oder Reinheit) an sich , sondern in der Fähigkeit, leistungsstarke, wiederverwendbare Funktionen zu schreiben und zu kombinieren.

Ein gutes Tutorial mit vielen Beispielen finden Sie in John Hughes 'Artikel Why Functional Programming Matters (PDF).

Sie werden Gobs produktive, vor allem , wenn Sie eine funktionale Sprache wählen , dass auch algebraische Datentypen und Musterabgleich (Caml, SML, Haskell) hat.

Norman Ramsey
quelle
Würden Mixins nicht auch wiederverwendbaren Code auf ähnliche Weise wie OOP bereitstellen? OOP nicht befürworten, nur versuchen, die Dinge selbst zu verstehen.
Mikemaccana
20

Viele der anderen Antworten haben sich auf die Performance-Seite (Parallelität) der funktionalen Programmierung konzentriert, was meiner Meinung nach sehr wichtig ist. Sie haben jedoch speziell nach der Produktivität gefragt, da Sie in einem funktionalen Paradigma dasselbe schneller programmieren können als in einem imperativen Paradigma.

Ich finde tatsächlich (aus persönlicher Erfahrung), dass das Programmieren in F # meiner Meinung nach besser entspricht und es daher einfacher ist. Ich denke, das ist der größte Unterschied. Ich habe sowohl in F # als auch in C # programmiert, und es gibt viel weniger "Kampf gegen die Sprache" in F #, was ich liebe. Sie müssen nicht über die Details in F # nachdenken. Hier sind einige Beispiele von dem, was mir wirklich Spaß macht.

Obwohl beispielsweise F # statisch typisiert ist (alle Typen werden zur Kompilierungszeit aufgelöst), ermittelt die Typinferenz, welche Typen Sie haben, sodass Sie es nicht sagen müssen. Und wenn es es nicht herausfinden kann, macht es automatisch Ihre Funktion / Klasse / was auch immer generisch. Sie müssen also nie etwas Generisches schreiben, es ist alles automatisch. Ich finde, das bedeutet, dass ich mehr Zeit damit verbringe, über das Problem nachzudenken und weniger darüber, wie ich es umsetzen kann. Wenn ich zu C # zurückkehre, vermisse ich diese Art von Folgerung wirklich. Man merkt nie, wie ablenkend sie ist, bis man sie nicht mehr tun muss.

Anstatt in Schleifen Schleifen zu schreiben, rufen Sie auch Funktionen auf. Es ist eine subtile Änderung, aber bedeutsam, weil Sie nicht mehr über das Schleifenkonstrukt nachdenken müssen. Hier ist zum Beispiel ein Code, der durchgeht und mit etwas übereinstimmt (ich kann mich nicht erinnern, was, es ist aus einem Projekt-Euler-Puzzle):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Mir ist klar, dass es recht einfach wäre, einen Filter und dann eine Karte (das ist eine Konvertierung jedes Elements) in C # durchzuführen, aber Sie müssen auf einer niedrigeren Ebene denken. Insbesondere müssten Sie die Schleife selbst schreiben und Ihre eigene explizite if-Anweisung und solche Dinge haben. Seit ich F # gelernt habe, habe ich festgestellt, dass es einfacher ist, auf funktionale Weise zu codieren. Wenn Sie filtern möchten, schreiben Sie "filter", und wenn Sie zuordnen möchten, schreiben Sie "map", anstatt zu implementieren jedes der Details.

Ich liebe auch den Operator |>, der meiner Meinung nach F # von ocaml und möglicherweise andere funktionale Sprachen trennt. Es ist der Pipe-Operator, mit dem Sie die Ausgabe eines Ausdrucks in die Eingabe eines anderen Ausdrucks "leiten" können. Es lässt den Code folgen, wie ich mehr denke. Wie im obigen Code-Snippet heißt es: "Nehmen Sie die Faktorenfolge, filtern Sie sie und ordnen Sie sie dann zu." Es ist ein sehr hohes Maß an Denken, das Sie nicht in einer zwingenden Programmiersprache erhalten, weil Sie so beschäftigt sind, die Schleife und if-Anweisungen zu schreiben. Es ist das Einzige, was ich am meisten vermisse, wenn ich in eine andere Sprache gehe.

Obwohl ich sowohl in C # als auch in F # programmieren kann, fällt es mir im Allgemeinen leichter, F # zu verwenden, da Sie auf einer höheren Ebene denken können. Ich würde argumentieren, dass ich produktiver bin, weil die kleineren Details aus der funktionalen Programmierung entfernt werden (zumindest in F #).

Bearbeiten : Ich habe in einem der Kommentare gesehen, dass Sie nach einem Beispiel für "state" in einer funktionalen Programmiersprache gefragt haben. F # kann zwingend geschrieben werden. Hier ist ein direktes Beispiel dafür, wie Sie einen veränderlichen Zustand in F # haben können:

let mutable x = 5
for i in 1..10 do
    x <- x + i
Ray Hidayat
quelle
1
Ich stimme Ihrem Beitrag im Allgemeinen zu, aber |> hat nichts mit funktionaler Programmierung an sich zu tun. Eigentlich a |> b (p1, p2)ist nur syntaktischer Zucker für b (a, p1, p2). Wenn Sie dies mit der richtigen Assoziativität verbinden, haben Sie es.
Anton Tykhyy
2
Es stimmt, ich sollte anerkennen, dass wahrscheinlich ein Großteil meiner positiven Erfahrungen mit F # mehr mit F # zu tun hat als mit funktionaler Programmierung. Trotzdem besteht eine starke Korrelation zwischen den beiden, und obwohl Dinge wie Typinferenz und |> per se keine funktionale Programmierung sind, würde ich mit Sicherheit behaupten, dass sie "mit dem Territorium gehen". Zumindest im Allgemeinen.
Ray Hidayat
|> ist nur eine weitere Infix-Funktion höherer Ordnung, in diesem Fall ein Funktionsanwendungsoperator. Das Definieren eigener Infix-Operatoren höherer Ordnung ist definitiv Teil der funktionalen Programmierung (es sei denn, Sie sind ein Schemer). Haskell hat das gleiche $, außer dass die Informationen in der Pipeline von rechts nach links fließen.
Norman Ramsey
15

Betrachten Sie all die schwierigen Fehler, die Sie lange Zeit beim Debuggen verbracht haben.

Wie viele dieser Fehler waren auf "unbeabsichtigte Interaktionen" zwischen zwei separaten Komponenten eines Programms zurückzuführen? (Fast alle Threading-Fehler haben diese Form: Rennen, bei denen gemeinsam genutzte Daten geschrieben werden, Deadlocks, ... Außerdem ist es üblich, Bibliotheken zu finden, die unerwartete Auswirkungen auf den globalen Status haben, oder die Registrierung / Umgebung zu lesen / schreiben usw.) I. würde davon ausgehen, dass mindestens 1 von 3 "Hard Bugs" in diese Kategorie fallen.

Wenn Sie jetzt zu zustandsloser / unveränderlicher / reiner Programmierung wechseln, verschwinden alle diese Fehler. Sie sind stattdessen mit einigen neuen Herausforderungen (zB wenn Sie tun wollen verschiedene Module zur Interaktion mit der Umwelt), aber in einer Sprache wie Haskell, erhalten diese Interaktionen explizit in das Typsystem verdinglicht, das bedeutet , dass Sie nur in der Art aussehen kann eine Funktion und ein Grund für die Art der Interaktionen, die es mit dem Rest des Programms haben kann.

Das ist der große Gewinn von "Unveränderlichkeit" IMO. In einer idealen Welt würden wir alle großartige APIs entwerfen, und selbst wenn die Dinge veränderlich wären, wären die Auswirkungen lokal und gut dokumentiert, und „unerwartete“ Interaktionen würden auf ein Minimum beschränkt. In der realen Welt gibt es viele APIs, die auf vielfältige Weise mit dem globalen Status interagieren, und diese sind die Quelle der schädlichsten Fehler. Das Streben nach Staatenlosigkeit strebt danach, unbeabsichtigte / implizite / Interaktionen hinter den Kulissen zwischen Komponenten loszuwerden.

Brian
quelle
6
Jemand hat einmal gesagt, dass das Überschreiben eines veränderlichen Werts bedeutet, dass Sie explizit den vorherigen Wert sammeln / freigeben. In einigen Fällen wurden andere Teile des Programms nicht mit diesem Wert ausgeführt. Wenn Werte nicht mutiert werden können, verschwindet auch diese Fehlerklasse.
Shapr
8

Ein Vorteil zustandsloser Funktionen besteht darin, dass sie eine Vorberechnung oder Zwischenspeicherung der Rückgabewerte der Funktion ermöglichen. Selbst bei einigen C-Compilern können Sie Funktionen explizit als zustandslos markieren, um ihre Optimierbarkeit zu verbessern. Wie viele andere angemerkt haben, lassen sich zustandslose Funktionen viel einfacher parallelisieren.

Effizienz ist jedoch nicht das einzige Problem. Eine reine Funktion ist einfacher zu testen und zu debuggen, da alles, was sie betrifft, explizit angegeben wird. Und wenn man in einer funktionalen Sprache programmiert, macht man es sich zur Gewohnheit, so wenige Funktionen (mit E / A usw.) wie möglich "schmutzig" zu machen. Das Stateful-Zeug auf diese Weise zu trennen, ist eine gute Möglichkeit, Programme zu entwerfen, selbst in nicht so funktionalen Sprachen.

Es kann eine Weile dauern, bis funktionale Sprachen "verstanden" werden, und es ist schwierig, sie jemandem zu erklären, der diesen Prozess nicht durchlaufen hat. Aber die meisten Menschen, die lange genug bestehen, erkennen schließlich, dass sich die Aufregung lohnt, auch wenn sie nicht viel funktionale Sprachen verwenden.

Artelius
quelle
Dieser erste Teil ist ein wirklich interessanter Punkt, darüber hatte ich noch nie nachgedacht. Vielen Dank!
Sasha Chedygov
Angenommen, Sie haben sin(PI/3)in Ihrem Code, wo PI eine Konstante ist, könnte der Compiler diese Funktion zur Kompilierungszeit auswerten und das Ergebnis in den generierten Code einbetten.
Artelius
6

Ohne Status ist es sehr einfach, Ihren Code automatisch zu parallelisieren (da CPUs mit immer mehr Kernen hergestellt werden, ist dies sehr wichtig).

Zifre
quelle
Ja, das habe ich mir definitiv angesehen. Insbesondere das Parallelitätsmodell von Erlang ist sehr faszinierend. An diesem Punkt ist mir die Parallelität jedoch weniger wichtig als die Produktivität. Gibt es einen Produktivitätsbonus bei der Programmierung ohne Status?
Sasha Chedygov
2
@musicfreak, nein, es gibt keinen Produktivitätsbonus. In modernen FP-Sprachen können Sie den Status jedoch weiterhin verwenden, wenn Sie ihn wirklich benötigen.
Unbekannt
"Ja wirklich?" Können Sie ein Beispiel für einen Zustand in einer funktionalen Sprache geben, damit ich sehen kann, wie es gemacht wird?
Sasha Chedygov
Schauen Sie sich die State Monad in Haskell an - book.realworldhaskell.org/read/monads.html#x_NZ
Rampion
4
@ Unbekannt: Ich bin anderer Meinung. Das Programmieren ohne Status reduziert das Auftreten von Fehlern, die auf unvorhergesehene / unbeabsichtigte Interaktionen verschiedener Komponenten zurückzuführen sind. Es fördert auch ein besseres Design (mehr Wiederverwendbarkeit, Trennung von Mechanismus und Politik und dergleichen). Es ist nicht immer für die jeweilige Aufgabe geeignet, aber in einigen Fällen strahlt es wirklich.
Artelius
6

Zustandslose Webanwendungen sind unerlässlich, wenn Sie mehr Verkehr haben.

Es kann viele Benutzerdaten geben, die Sie beispielsweise aus Sicherheitsgründen nicht auf der Clientseite speichern möchten. In diesem Fall müssen Sie es serverseitig speichern. Sie können die Standardsitzung der Webanwendungen verwenden. Wenn Sie jedoch mehr als eine Instanz der Anwendung haben, müssen Sie sicherstellen, dass jeder Benutzer immer auf dieselbe Instanz verwiesen wird.

Load Balancer haben häufig die Möglichkeit, "Sticky Sessions" durchzuführen, bei denen der Load Balancer weiß, an welchen Server die Benutzeranforderung gesendet werden soll. Dies ist jedoch nicht ideal. Beispielsweise bedeutet dies, dass bei jedem Neustart Ihrer Webanwendung alle verbundenen Benutzer ihre Sitzung verlieren.

Ein besserer Ansatz besteht darin, die Sitzung hinter den Webservern in einer Art Datenspeicher zu speichern. Heutzutage stehen dafür viele großartige NOSQL-Produkte zur Verfügung (Redis, Mongo, Elasticsearch, Memcached). Auf diese Weise sind die Webserver zustandslos, aber Sie haben immer noch den Status serverseitig, und die Verfügbarkeit dieses Status kann durch Auswahl des richtigen Datenspeicher-Setups verwaltet werden. Diese Datenspeicher weisen normalerweise eine hohe Redundanz auf, sodass es fast immer möglich sein sollte, Änderungen an Ihrer Webanwendung und sogar am Datenspeicher vorzunehmen, ohne die Benutzer zu beeinträchtigen.

shmish111
quelle