Wie können Sie etwas Nützliches ohne veränderlichen Zustand tun?


265

Ich habe in letzter Zeit viel über funktionale Programmierung gelesen und kann das meiste davon verstehen, aber das einzige, was ich einfach nicht in den Kopf bekommen kann, ist zustandslose Codierung. Es scheint mir, dass die Vereinfachung der Programmierung durch Entfernen des veränderlichen Zustands wie das "Vereinfachen" eines Autos durch Entfernen des Armaturenbretts ist: Das fertige Produkt mag einfacher sein, aber viel Glück bei der Interaktion mit den Endbenutzern.

Nahezu jede Benutzeranwendung, die ich mir vorstellen kann, beinhaltet den Status als Kernkonzept. Wenn Sie ein Dokument (oder einen SO-Beitrag) schreiben, ändert sich der Status mit jeder neuen Eingabe. Oder wenn Sie ein Videospiel spielen, gibt es Unmengen von Statusvariablen, beginnend mit den Positionen aller Charaktere, die dazu neigen, sich ständig zu bewegen. Wie können Sie möglicherweise etwas Nützliches tun, ohne die sich ändernden Werte im Auge zu behalten?

Jedes Mal, wenn ich etwas finde, das dieses Problem behandelt, ist es in einer wirklich technischen Funktion geschrieben, die einen starken FP-Hintergrund voraussetzt, den ich nicht habe. Kennt jemand eine Möglichkeit, dies jemandem mit einem guten, soliden Verständnis der imperativen Codierung zu erklären, aber wer ist ein vollständiger n00b auf der funktionalen Seite?

EDIT: Einige der bisherigen Antworten scheinen mich von den Vorteilen unveränderlicher Werte zu überzeugen. Ich verstehe diesen Teil. Es macht vollkommen Sinn. Was ich nicht verstehe, ist, wie Sie Werte verfolgen können, die sich ändern müssen und sich ständig ändern, ohne veränderbare Variablen.



1
Meine persönliche bescheidene Meinung ist, dass es wie Stärke und Geld ist. Es gilt das Gesetz der Ertragsminderung. Wenn Sie ziemlich stark sind, gibt es vielleicht wenig Anreiz, etwas stärker zu werden, aber es tut nicht weh, daran zu arbeiten (und manche Menschen tun dies mit Leidenschaft). Gleiches gilt für den globalen veränderlichen Zustand. Es ist meine persönliche Präferenz zu akzeptieren, dass es im Verlauf meiner Codierungsfähigkeiten gut ist, die Menge des globalen veränderlichen Status in meinem Code zu begrenzen. Es mag niemals perfekt sein, aber es ist gut, auf die Minimierung des globalen veränderlichen Zustands hinzuarbeiten.
AturSams

Wie beim Geld wird ein Punkt erreicht, an dem mehr Zeit investiert wird, der nicht mehr sehr nützlich ist und andere Prioritäten an die Spitze rücken. Wenn Sie zum Beispiel die größtmögliche Stärke erreichen (gemäß meiner Metapher), hat dies möglicherweise keinen nützlichen Zweck und kann sogar zu einer Belastung werden. Aber es ist immer noch gut, dieses möglicherweise unerreichbare Ziel anzustreben und moderate Ressourcen in es zu investieren.
AturSams

7
Kurz gesagt, in FP ändern Funktionen niemals den Status. Schließlich geben sie etwas zurück, das den aktuellen Status ersetzt. Aber der Staat wird niemals an Ort und Stelle verändert (mutiert).
Jinglesthula

Es gibt Möglichkeiten, ohne Mutation Statefulness zu erreichen (unter Verwendung des Stacks, soweit ich weiß), aber diese Frage ist in gewissem Sinne nebensächlich (auch wenn sie großartig ist). Es ist schwer, kurz und bündig darüber zu sprechen, aber hier ist ein Beitrag, der hoffentlich Ihre Frage beantwortet: medium.com/@jbmilgrom/… . Das TLDR ist, dass die Semantik selbst eines zustandsbehafteten Funktionsprogramms unveränderlich ist, jedoch Kommunikations-S / W-Läufe der Programmfunktion behandelt werden.
jbmilgrom

Antworten:


166

Oder wenn Sie ein Videospiel spielen, gibt es Unmengen von Statusvariablen, beginnend mit den Positionen aller Charaktere, die dazu neigen, sich ständig zu bewegen. Wie können Sie möglicherweise etwas Nützliches tun, ohne die sich ändernden Werte im Auge zu behalten?

Wenn Sie interessiert sind, finden Sie hier eine Reihe von Artikeln, die die Spielprogrammierung mit Erlang beschreiben.

Diese Antwort wird Ihnen wahrscheinlich nicht gefallen, aber Sie erhalten erst dann ein funktionierendes Programm, wenn Sie es verwenden. Ich kann Codebeispiele posten und sagen : „Hier, nicht wahr sehen “ - aber wenn Sie die Syntax und die zugrunde liegenden Prinzipien nicht verstehen, dann die Augen glasig gerade. Aus Ihrer Sicht sieht es so aus, als würde ich das Gleiche tun wie eine imperative Sprache, aber nur alle Arten von Grenzen setzen, um das Programmieren gezielt zu erschweren. Meiner Ansicht nach erleben Sie gerade das Blub-Paradoxon .

Anfangs war ich skeptisch, aber ich bin vor ein paar Jahren in den funktionalen Programmierzug gestiegen und habe mich in ihn verliebt. Der Trick bei der funktionalen Programmierung besteht darin, Muster und bestimmte Variablenzuweisungen zu erkennen und den imperativen Zustand auf den Stapel zu verschieben. Eine for-Schleife wird beispielsweise zur Rekursion:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

Es ist nicht sehr hübsch, aber wir haben den gleichen Effekt ohne Mutation. Natürlich vermeiden wir, wo immer möglich, das Schleifen ganz und abstrahieren es einfach weg:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Die Seq.iter-Methode zählt die Sammlung auf und ruft die anonyme Funktion für jedes Element auf. Sehr praktisch :)

Ich weiß, das Drucken von Zahlen ist nicht gerade beeindruckend. Bei Spielen können wir jedoch denselben Ansatz verwenden: Halten Sie den gesamten Status im Stapel und erstellen Sie ein neues Objekt mit unseren Änderungen im rekursiven Aufruf. Auf diese Weise ist jeder Frame eine zustandslose Momentaufnahme des Spiels, wobei jeder Frame einfach ein brandneues Objekt mit den gewünschten Änderungen aller zustandslosen Objekte erstellt, die aktualisiert werden müssen. Der Pseudocode hierfür könnte sein:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

Die imperative und die funktionale Version sind identisch, aber die funktionale Version verwendet eindeutig keinen veränderlichen Zustand. Der Funktionscode hält den gesamten Status auf dem Stapel - das Schöne an diesem Ansatz ist, dass das Debuggen einfach ist, wenn etwas schief geht. Sie benötigen lediglich eine Stapelverfolgung.

Dies lässt sich auf eine beliebige Anzahl von Objekten im Spiel skalieren, da alle Objekte (oder Sammlungen verwandter Objekte) in einem eigenen Thread gerendert werden können.

Nahezu jede Benutzeranwendung, die ich mir vorstellen kann, beinhaltet den Status als Kernkonzept.

In funktionalen Sprachen geben wir einfach ein neues Objekt mit den gewünschten Änderungen zurück, anstatt den Status von Objekten zu ändern. Es ist effizienter als es klingt. Datenstrukturen sind beispielsweise sehr einfach als unveränderliche Datenstrukturen darzustellen. Stapel sind beispielsweise notorisch einfach zu implementieren:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Der obige Code erstellt zwei unveränderliche Listen, hängt sie zusammen, um eine neue Liste zu erstellen, und hängt die Ergebnisse an. In der gesamten Anwendung wird kein veränderlicher Status verwendet. Es sieht etwas sperrig aus, aber das liegt nur daran, dass C # eine ausführliche Sprache ist. Hier ist das entsprechende Programm in F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Keine Änderung erforderlich, um Listen zu erstellen und zu bearbeiten. Nahezu alle Datenstrukturen können problemlos in ihre Funktionsäquivalente umgewandelt werden. Ich habe hier eine Seite geschrieben , die unveränderliche Implementierungen von Stapeln, Warteschlangen, linken Haufen, rot-schwarzen Bäumen und faulen Listen enthält. Kein einziger Codeausschnitt enthält einen veränderlichen Status. Um einen Baum zu "mutieren", erstelle ich einen brandneuen mit einem neuen Knoten, den ich möchte. Dies ist sehr effizient, da ich nicht von jedem Knoten im Baum eine Kopie erstellen muss, sondern die alten in meinem neuen wiederverwenden kann Baum.

Anhand eines aussagekräftigeren Beispiels habe ich auch diesen SQL-Parser geschrieben, der völlig zustandslos ist (oder zumindest mein Code ist zustandslos, ich weiß nicht, ob die zugrunde liegende Lexing-Bibliothek zustandslos ist).

Staatenloses Programmieren ist genauso ausdrucksstark und leistungsfähig wie zustandsloses Programmieren. Es erfordert nur ein wenig Übung, um sich darin zu üben, staatenlos zu denken. Natürlich scheint "zustandslose Programmierung, wenn möglich, zustandsbehaftete Programmierung, wo nötig" das Motto der unreinsten funktionalen Sprachen zu sein. Es schadet nicht, auf veränderliche Elemente zurückzugreifen, wenn der funktionale Ansatz nicht so sauber oder effizient ist.


7
Ich mag das Pacman-Beispiel. Aber das könnte ein Problem lösen, nur um ein anderes aufzuwerfen: Was ist, wenn etwas anderes einen Verweis auf das vorhandene Pacman-Objekt enthält? Dann wird der Müll nicht gesammelt und ersetzt. Stattdessen erhalten Sie zwei Kopien des Objekts, von denen eine ungültig ist. Wie gehen Sie mit diesem Problem um?
Mason Wheeler

9
Natürlich müssen Sie mit dem neuen Pacman-Objekt ein neues "etwas anderes" erstellen;) Wenn wir diesen Weg zu weit gehen, erstellen wir natürlich jedes Mal das Objektdiagramm für unsere gesamte Welt neu, wenn sich etwas ändert. Ein besserer Ansatz wird hier beschrieben ( prog21.dadgum.com/26.html ): Anstatt dass Objekte sich selbst und alle ihre Abhängigkeiten aktualisieren, ist es viel einfacher, wenn sie Nachrichten über ihren Status an eine Ereignisschleife übergeben, die alle Ereignisse verarbeitet Aktualisierung. Dies erleichtert die Entscheidung, welche Objekte im Diagramm aktualisiert werden müssen und welche nicht.
Julia

6
@Juliet, ich habe einen Zweifel - in meiner absolut zwingenden Denkweise muss die Rekursion irgendwann enden, sonst wird es irgendwann zu einem Stapelüberlauf kommen. Wie wird im rekursiven Pacman-Beispiel der Stapel in Schach gehalten - wird das Objekt implizit am Anfang der Funktion eingeblendet?
BlueStrat

9
@BlueStrat - gute Frage ... wenn es sich um einen "Tail Call" handelt ... dh der rekursive Aufruf ist das Letzte in der Funktion ... dann muss das System keinen neuen Stack Frame generieren ... das kann es Verwenden Sie einfach die vorherige wieder. Dies ist eine gängige Optimierung für funktionale Programmiersprachen. en.wikipedia.org/wiki/Tail_call
Reteptilian

4
@MichaelOsofsky Bei der Interaktion mit Datenbanken und APIs gibt es immer eine Außenwelt, mit der kommuniziert werden kann. In diesem Fall können Sie nicht 100% funktionsfähig sein. Es ist wichtig, diesen "nicht funktionierenden" Code isoliert und abstrahiert zu halten, damit es nur einen Eingang und einen Ausgang zur Außenwelt gibt. Auf diese Weise können Sie den Rest Ihres Codes funktionsfähig halten.
Chielt

76

Kurze Antwort: Sie können nicht.

Was ist dann die Aufregung um Unveränderlichkeit?

Wenn Sie sich mit der imperativen Sprache auskennen, wissen Sie, dass "Globale schlecht sind". Warum? Weil sie einige sehr schwer zu entwirrende Abhängigkeiten in Ihren Code einführen (oder einführen können). Und Abhängigkeiten sind nicht gut; Sie möchten, dass Ihr Code modular aufgebaut ist . Programmteile beeinflussen andere Teile nicht so wenig wie möglich. Und FP bringt Sie zu den heiligen Gral der Modularität: keine Nebenwirkungen überhaupt . Sie haben nur Ihr f (x) = y. Gib x rein, hol y raus. Keine Änderungen an x ​​oder irgendetwas anderem. Mit FP hören Sie auf, über den Zustand nachzudenken, und beginnen, in Werten zu denken. Alle Ihre Funktionen empfangen einfach Werte und erzeugen neue Werte.

Dies hat mehrere Vorteile.

Zunächst einmal bedeuten keine Nebenwirkungen einfachere Programme, über die man leichter nachdenken kann. Keine Sorge, dass die Einführung eines neuen Programmteils einen vorhandenen, funktionierenden Teil stören und zum Absturz bringen wird.

Zweitens macht dies das Programm trivial parallelisierbar (eine effiziente Parallelisierung ist eine andere Sache).

Drittens gibt es einige mögliche Leistungsvorteile. Angenommen, Sie haben eine Funktion:

double x = 2 * x

Jetzt geben Sie einen Wert von 3 ein und Sie erhalten einen Wert von 6 aus. Jedes Mal. Aber das können Sie auch unbedingt tun, oder? Ja. Das Problem ist jedoch, dass Sie unbedingt noch mehr tun können . Ich kann:

int y = 2;
int double(x){ return x * y; }

aber ich könnte es auch tun

int y = 2;
int double(x){ return x * (y++); }

Der imperative Compiler weiß nicht, ob ich Nebenwirkungen haben werde oder nicht, was die Optimierung schwieriger macht (dh Double 2 muss nicht jedes Mal 4 sein). Der Funktionale weiß, dass ich es nicht tun werde - daher kann er jedes Mal optimieren, wenn er "double 2" sieht.

Obwohl das Erstellen neuer Werte jedes Mal für komplexe Wertetypen in Bezug auf den Computerspeicher unglaublich verschwenderisch erscheint, muss dies nicht der Fall sein. Denn wenn Sie f (x) = y haben und die Werte x und y "meistens gleich" sind (z. B. Bäume, die sich nur in wenigen Blättern unterscheiden), können x und y Teile des Speichers gemeinsam nutzen - da keiner von beiden mutiert .

Also, wenn diese unveränderliche Sache so großartig ist, warum habe ich geantwortet, dass man ohne veränderlichen Zustand nichts Nützliches tun kann? Nun, ohne Veränderbarkeit wäre Ihr gesamtes Programm eine riesige f (x) = y-Funktion. Und das Gleiche gilt für alle Teile Ihres Programms: nur Funktionen und Funktionen im "reinen" Sinne. Wie gesagt, dies bedeutet jedes Mal f (x) = y . So müsste beispielsweise readFile ("myFile.txt") jedes Mal den gleichen Zeichenfolgenwert zurückgeben. Nicht allzu nützlich.

Daher bietet jedes FP ein Mittel zum Mutieren des Zustands. "Reine" funktionale Sprachen (z. B. Haskell) tun dies mit etwas beängstigenden Konzepten wie Monaden, während "unreine" (z. B. ML) dies direkt zulassen.

Und natürlich kommen funktionale Sprachen mit einer Vielzahl anderer Extras, die die Programmierung effizienter machen, wie erstklassige Funktionen usw.


2
<< readFile ("myFile.txt") müsste jedes Mal den gleichen Zeichenfolgenwert zurückgeben. Nicht allzu nützlich. >> Ich vermute, es ist nützlich, solange Sie das globale Dateisystem verstecken. Wenn Sie es als zweiten Parameter betrachten und andere Prozesse jedes Mal, wenn sie es mit filesystem2 = write (Dateisystem1, fd, pos, "string") ändern, einen neuen Verweis auf das Dateisystem zurückgeben, lassen Sie alle Prozesse ihren Verweis auf das Dateisystem austauschen könnten wir ein viel saubereres Bild des Betriebssystems bekommen.
Aal Gheez

@eelghEEz, dies ist der gleiche Ansatz, den Datomic für Datenbanken verfolgt.
Jason

1
+1 für den klaren und prägnanten Vergleich zwischen Paradigmen. Ein Vorschlag ist, int double(x){ return x * (++y); }da der aktuelle immer noch 4 sein wird, obwohl er immer noch eine unangekündigte Nebenwirkung hat, während er ++y6
zurückgibt

@eelghEEz Ich bin mir nicht sicher, ob es eine Alternative gibt, oder? Um Informationen in einen (reinen) FP-Kontext einzuführen, "nehmen Sie eine Messung vor", z. B. "zum Zeitstempel X ist die Temperatur Y". Wenn jemand nach der Temperatur fragt, kann er implizit X = jetzt bedeuten, aber er kann unmöglich nach der Temperatur als universelle Funktion der Zeit fragen, oder? FP befasst sich mit unveränderlichen Zuständen, und Sie müssen einen unveränderlichen Zustand - aus internen und externen Quellen - aus einem veränderlichen erstellen . Indizes, Zeitstempel usw. sind nützlich, aber orthogonal zur Veränderlichkeit - wie VCS zur Versionskontrolle selbst.
John P

29

Beachten Sie, dass die Aussage, dass funktionale Programmierung keinen Status hat, etwas irreführend ist und die Ursache für die Verwirrung sein kann. Es hat definitiv keinen "veränderlichen Zustand", aber es kann immer noch Werte haben, die manipuliert werden. Sie können einfach nicht direkt geändert werden (z. B. müssen Sie aus den alten Werten neue Werte erstellen).

Dies ist eine grobe Vereinfachung, aber stellen Sie sich vor, Sie hätten eine OO-Sprache, in der alle Eigenschaften von Klassen nur einmal im Konstruktor festgelegt werden. Alle Methoden sind statische Funktionen. Sie können so ziemlich jede Berechnung durchführen, indem Methoden Objekte verwenden, die alle Werte enthalten, die sie für ihre Berechnungen benötigen, und dann neue Objekte mit dem Ergebnis zurückgeben (möglicherweise sogar eine neue Instanz desselben Objekts).

Es mag "schwierig" sein, vorhandenen Code in dieses Paradigma zu übersetzen, aber das liegt daran, dass es wirklich eine völlig andere Art des Denkens über Code erfordert. Als Nebeneffekt erhalten Sie jedoch in den meisten Fällen viele kostenlose Parallelitätsmöglichkeiten.

Nachtrag: (In Bezug auf Ihre Bearbeitung, wie
Sie die Werte verfolgen können, die geändert werden müssen) Sie würden natürlich in einer unveränderlichen Datenstruktur gespeichert ...

Dies ist keine vorgeschlagene "Lösung", aber der einfachste Weg, um zu sehen, dass dies immer funktioniert, besteht darin, dass Sie diese unveränderlichen Werte in einer kartenähnlichen Struktur (Wörterbuch / Hashtabelle) speichern können, die mit einem "Variablennamen" versehen ist.

Natürlich würden Sie in praktischen Lösungen einen vernünftigeren Ansatz verwenden, aber dies zeigt, dass Sie im schlimmsten Fall, wenn nichts anderes funktionieren würde, den veränderlichen Zustand mit einer solchen Karte "simulieren" könnten, die Sie durch Ihren Aufrufbaum tragen.


2
OK, ich habe den Titel geändert. Ihre Antwort scheint jedoch zu einem noch schlimmeren Problem zu führen. Wenn ich jedes Objekt jedes Mal neu erstellen muss, wenn sich etwas in seinem Status ändert, verbringe ich meine gesamte CPU-Zeit damit, nur Objekte zu erstellen. Ich denke hier an die Spielprogrammierung, bei der sich viele Dinge gleichzeitig auf dem Bildschirm (und außerhalb des Bildschirms) bewegen, die in der Lage sein müssen, miteinander zu interagieren. Die gesamte Engine hat eine festgelegte Framerate: Alles, was Sie tun werden, müssen Sie in X Millisekunden tun. Sicher gibt es einen besseren Weg, als ständig ganze Objekte zu recyceln?
Mason Wheeler

4
Das Schöne daran ist, dass die Unveränderlichkeit auf der Sprache und nicht auf der Implementierung liegt. Mit ein paar Tricks können Sie einen unveränderlichen Status in der Sprache haben, während die Implementierung tatsächlich den Status ändert. Siehe zum Beispiel Haskells ST-Monade.
CesarB

4
@Mason: Der Punkt ist, dass der Compiler viel besser entscheiden kann, wo es (thread) sicher ist, den Status an Ort und Stelle zu ändern, als Sie können.
Jerryjvl

Ich denke, für Spiele sollten Sie es vermeiden, für Teile unveränderlich zu sein, bei denen Geschwindigkeit keine Rolle spielt. Während eine unveränderliche Sprache für Sie optimiert werden kann, ist nichts schneller als das Ändern des Speichers, was CPUs schnell können. Wenn sich herausstellt, dass es 10 oder 20 Stellen gibt, an denen Sie unbedingt etwas benötigen, sollten Sie unveränderlich vermeiden, es sei denn, Sie können es für sehr getrennte Bereiche wie Spielmenüs modularisieren. Insbesondere die Spielelogik könnte ein guter Ort sein, um unveränderlich zu arbeiten, da ich der Meinung bin, dass sie sich hervorragend für die komplexe Modellierung reiner Systeme wie Geschäftsregeln eignet.
LegendLength

@LegendLength widersprechen Sie sich.
Ixx

18

Ich denke, es gibt ein leichtes Missverständnis. Reine Funktionsprogramme haben Status. Der Unterschied besteht darin, wie dieser Zustand modelliert wird. Bei der reinen Funktionsprogrammierung wird der Status durch Funktionen manipuliert, die einen bestimmten Status annehmen und den nächsten Status zurückgeben. Die Sequenzierung durch Zustände wird dann erreicht, indem der Zustand durch eine Folge von reinen Funktionen geleitet wird.

Sogar der globale veränderbare Zustand kann auf diese Weise modelliert werden. In Haskell zum Beispiel ist ein Programm eine Funktion von einer Welt zur anderen. Das heißt, Sie übergeben das gesamte Universum und das Programm gibt ein neues Universum zurück. In der Praxis müssen Sie jedoch nur die Teile des Universums übergeben, an denen Ihr Programm tatsächlich interessiert ist. Und Programme geben tatsächlich eine Folge von Aktionen zurück , die als Anweisungen für die Betriebsumgebung dienen, in der das Programm ausgeführt wird.

Sie wollten dies anhand der imperativen Programmierung erklären. OK, schauen wir uns eine wirklich einfache imperative Programmierung in einer funktionalen Sprache an.

Betrachten Sie diesen Code:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Ziemlich zwingender Standard-Imperativcode. Macht nichts Interessantes, aber das ist zur Veranschaulichung in Ordnung. Ich denke, Sie werden zustimmen, dass es hier um einen Staat geht. Der Wert der Variablen x ändert sich mit der Zeit. Lassen Sie uns nun die Notation leicht ändern, indem wir eine neue Syntax erfinden:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Setzen Sie Klammern, um klarer zu machen, was dies bedeutet:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

Sie sehen, der Zustand wird durch eine Folge von reinen Ausdrücken modelliert, die die freien Variablen der folgenden Ausdrücke binden.

Sie werden feststellen, dass dieses Muster jede Art von Zustand modellieren kann, sogar E / A.


Ist das wie eine Monade?
CMCDragonkai

Würden Sie dies in Betracht ziehen: A ist auf Stufe 1 deklarativ B ist auf Stufe 2 deklarativ, es betrachtet A als zwingend. C ist auf Stufe 3 deklarativ, es betrachtet B als zwingend. Wenn wir die Abstraktionsschicht vergrößern, werden Sprachen, die auf der Abstraktionsschicht niedriger sind, immer als zwingender angesehen als sie selbst.
CMCDragonkai

14

So schreiben Sie Code ohne veränderlichen Status : Anstatt den sich ändernden Status in veränderbare Variablen umzuwandeln, fügen Sie ihn in die Parameter von Funktionen ein. Und anstatt Schleifen zu schreiben, schreiben Sie rekursive Funktionen. So zum Beispiel dieser zwingende Code:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

wird dieser Funktionscode (Schema-ähnliche Syntax):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

oder dieser Haskellish Code

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

In Bezug auf , warum funktionale Programmierer mögen , dies zu tun (was man nicht fragen), desto mehr Teile des Programms sind staatenlos, desto mehr Möglichkeiten gibt es Stücke zusammen zu stellen , ohne etwas Pause . 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 .


13

Es sind nur verschiedene Arten, dasselbe zu tun.

Stellen Sie sich ein einfaches Beispiel vor, wie das Hinzufügen der Zahlen 3, 5 und 10. Stellen Sie sich vor, Sie ändern den Wert von 3, indem Sie 5 hinzufügen, dann 10 zu dieser "3" hinzufügen und dann den aktuellen Wert von "ausgeben. 3 "(18). Dies scheint offensichtlich lächerlich, aber es ist im Wesentlichen die Art und Weise, wie staatsbasierte imperative Programmierung häufig durchgeführt wird. In der Tat können Sie viele verschiedene "3" haben, die den Wert 3 haben, aber unterschiedlich sind. All dies scheint seltsam, weil wir so tief in der enorm vernünftigen Idee verwurzelt sind, dass die Zahlen unveränderlich sind.

Denken Sie nun daran, 3, 5 und 10 hinzuzufügen, wenn Sie die Werte als unveränderlich betrachten. Sie addieren 3 und 5, um einen weiteren Wert zu erzeugen, 8, und addieren 10 zu diesem Wert, um einen weiteren Wert zu erzeugen, 18.

Dies sind äquivalente Methoden, um dasselbe zu tun. Alle erforderlichen Informationen sind in beiden Methoden vorhanden, jedoch in unterschiedlicher Form. In einem existieren die Informationen als Zustand und in den Regeln zum Ändern des Zustands. Im anderen Fall liegen die Informationen in unveränderlichen Daten und Funktionsdefinitionen vor.


10

Ich komme zu spät zur Diskussion, aber ich wollte ein paar Punkte für Leute hinzufügen, die mit funktionaler Programmierung zu kämpfen haben.

  1. Funktionssprachen behalten genau die gleichen Statusaktualisierungen bei wie imperative Sprachen, aber sie übergeben den aktualisierten Status an nachfolgende Funktionsaufrufe . Hier ist ein sehr einfaches Beispiel für das Befahren einer Zahlenreihe. Ihr Bundesstaat ist Ihr aktueller Standort.

Zuerst der imperative Weg (im Pseudocode)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

Nun der funktionale Weg (im Pseudocode). Ich stütze mich stark auf den ternären Operator, weil ich möchte, dass Menschen mit zwingendem Hintergrund diesen Code tatsächlich lesen können. Wenn Sie den ternären Operator also nicht häufig verwenden (ich habe ihn in meinen zwingenden Tagen immer vermieden), funktioniert er folgendermaßen.

predicate ? if-true-expression : if-false-expression

Sie können den ternären Ausdruck verketten, indem Sie anstelle des falschen Ausdrucks einen neuen ternären Ausdruck einfügen

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

In diesem Sinne ist hier die funktionale Version.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

Dies ist ein triviales Beispiel. Wenn dies Menschen in einer Spielwelt bewegen würde, müssten Sie Nebenwirkungen wie das Zeichnen der aktuellen Position des Objekts auf dem Bildschirm und eine gewisse Verzögerung bei jedem Aufruf einführen, je nachdem, wie schnell sich das Objekt bewegt. Aber Sie würden immer noch keinen veränderlichen Zustand brauchen.

  1. Die Lektion ist, dass funktionale Sprachen den Status "mutieren", indem sie die Funktion mit verschiedenen Parametern aufrufen. Natürlich mutiert dies keine Variablen, aber so erhalten Sie einen ähnlichen Effekt. Dies bedeutet, dass Sie sich daran gewöhnen müssen, rekursiv zu denken, wenn Sie funktionale Programmierung durchführen möchten.

  2. Rekursives Denken zu lernen ist nicht schwer, erfordert jedoch sowohl Übung als auch ein Toolkit. Dieser kleine Abschnitt in diesem "Java lernen" -Buch, in dem die Rekursion zur Berechnung der Fakultät verwendet wurde, schneidet ihn nicht ab. Sie benötigen ein Toolkit mit Fähigkeiten wie dem Erstellen iterativer Prozesse aus der Rekursion (aus diesem Grund ist die Schwanzrekursion für die funktionale Sprache unerlässlich), Fortsetzungen, Invarianten usw. Sie würden keine OO-Programmierung durchführen, ohne etwas über Zugriffsmodifikatoren, Schnittstellen usw. zu lernen zur funktionalen Programmierung.

Meine Empfehlung ist, den kleinen Schemer zu machen (beachten Sie, dass ich "tun" und nicht "lesen" sage) und dann alle Übungen in SICP zu machen. Wenn Sie fertig sind, haben Sie ein anderes Gehirn als zu Beginn.


8

Es ist in der Tat ziemlich einfach, etwas zu haben, das wie ein veränderlicher Zustand aussieht, selbst in Sprachen ohne veränderlichen Zustand.

Betrachten Sie eine Funktion mit Typ s -> (a, s). Das Übersetzen von Haskell Syntax, bedeutet dies eine Funktion , die einen Parameter vom Typ „nimmt s“ und gibt ein Paar von Werten, dem Typs „ a“ und „ s“. Wenn dies sder Typ unseres Status ist, nimmt diese Funktion einen Status an und gibt einen neuen Status und möglicherweise einen Wert zurück (Sie können jederzeit "unit" aka (), was voidin C / C ++ " a" entspricht , als " " zurückgeben). Art). Wenn Sie mehrere Funktionsaufrufe mit solchen Typen verketten (den Status von einer Funktion zurückgeben und an die nächste übergeben), haben Sie einen "veränderlichen" Status (tatsächlich erstellen Sie in jeder Funktion einen neuen Status und geben den alten auf ).

Es ist möglicherweise einfacher zu verstehen, wenn Sie sich den veränderlichen Zustand als den "Raum" vorstellen, in dem Ihr Programm ausgeführt wird, und dann an die Zeitdimension denken. Zum Zeitpunkt t1 befindet sich der "Raum" in einem bestimmten Zustand (zum Beispiel hat ein Speicherplatz den Wert 5). Zu einem späteren Zeitpunkt t2 befindet es sich in einem anderen Zustand (zum Beispiel hat dieser Speicherort jetzt den Wert 10). Jede dieser Zeit- "Scheiben" ist ein Zustand und unveränderlich (Sie können nicht in der Zeit zurückgehen, um sie zu ändern). Unter diesem Gesichtspunkt sind Sie also von der vollen Raumzeit mit einem Zeitpfeil (Ihrem veränderlichen Zustand) zu einer Reihe von Raumzeitscheiben (mehreren unveränderlichen Zuständen) übergegangen, und Ihr Programm behandelt jede Schicht nur als Wert und berechnet jede von ihnen als eine Funktion auf die vorherige angewendet.

OK, vielleicht war das nicht einfacher zu verstehen :-)

Es erscheint möglicherweise unzulänglich, den gesamten Programmstatus explizit als Wert darzustellen, der nur erstellt werden muss, um im nächsten Moment verworfen zu werden (unmittelbar nachdem ein neuer erstellt wurde). Für einige Algorithmen mag es natürlich sein, aber wenn dies nicht der Fall ist, gibt es einen anderen Trick. Anstelle eines realen Zustands können Sie einen gefälschten Zustand verwenden, der nichts anderes als ein Marker ist (nennen wir den Typ dieses gefälschten Zustands State#). Dieser gefälschte Zustand existiert aus Sicht der Sprache und wird wie jeder andere Wert übergeben, aber der Compiler lässt ihn beim Generieren des Maschinencodes vollständig weg. Es dient nur dazu, die Reihenfolge der Ausführung zu markieren.

Angenommen, der Compiler bietet uns die folgenden Funktionen:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

Wenn Sie aus diesen Haskell-ähnlichen Deklarationen übersetzen, readReferhalten Sie etwas, das einem Zeiger oder einem Handle auf einen Wert vom Typ " a" und den falschen Status ähnelt , und geben den Wert vom Typ " a" zurück, auf den der erste Parameter und ein neuer gefälschter Status zeigen. writeRefist ähnlich, ändert jedoch den Wert, auf den stattdessen verwiesen wird.

Wenn Sie readRefden von zurückgegebenen falschen Status aufrufen und dann übergeben writeRef(möglicherweise bei anderen Aufrufen nicht verwandter Funktionen in der Mitte; diese Statuswerte bilden eine "Kette" von Funktionsaufrufen), wird der geschriebene Wert zurückgegeben. Sie können writeReferneut mit demselben Zeiger / Handle aufrufen, und es wird an denselben Speicherort geschrieben. Da jedoch konzeptionell ein neuer (gefälschter) Status zurückgegeben wird, ist der (gefälschte) Status immer noch unveränderlich (ein neuer wurde "erstellt" "). Der Compiler ruft die Funktionen in der Reihenfolge auf, in der er sie aufrufen müsste, wenn eine reale Statusvariable berechnet werden müsste, aber der einzige Status, der vorhanden ist, ist der vollständige (veränderbare) Status der realen Hardware.

(Diejenigen , die Haskell wissen, merke ich , die Dinge vereinfacht viel und ommited einige wichtige Details. Für diejenigen , die wollen mehr Details zu sehen, zu sehen , Control.Monad.Statevon der mtlund an den ST sund IO(aka ST RealWorld) Monaden.)

Sie fragen sich vielleicht, warum Sie dies so umständlich tun (anstatt einfach einen veränderlichen Zustand in der Sprache zu haben). Der eigentliche Vorteil ist, dass Sie den Status Ihres Programms geändert haben . Was vorher implizit war (Ihr Programmstatus war global und erlaubte Dinge wie Fernwirkung ), ist jetzt explizit. Funktionen, die den Status nicht empfangen und zurückgeben, können ihn nicht ändern oder von ihm beeinflusst werden. Sie sind "rein". Noch besser ist, dass Sie separate Status-Threads haben können und mit ein wenig Typ-Magie eine imperative Berechnung in eine reine einbetten können, ohne sie unrein zu machen (die STMonade in Haskell wird normalerweise für diesen Trick verwendet; Das State#oben erwähnte ist in der Tat GHC State# s, das von seiner Implementierung des STund verwendet wirdIO Monaden).


7

Funktionale Programmierung vermeidet Zustand und betontFunktionalität. Es gibt nie einen Staat, obwohl der Staat tatsächlich etwas ist, das unveränderlich ist oder in die Architektur dessen eingebettet ist, mit dem Sie arbeiten. Betrachten Sie den Unterschied zwischen einem statischen Webserver, der nur Dateien aus dem Dateisystem lädt, und einem Programm, das einen Rubik-Cube implementiert. Ersteres wird in Form von Funktionen implementiert, mit denen eine Anforderung in eine Dateipfadanforderung in eine Antwort aus dem Inhalt dieser Datei umgewandelt werden soll. Über einen winzigen Teil der Konfiguration hinaus wird praktisch kein Status benötigt (der Dateisystem-Status liegt wirklich außerhalb des Programmbereichs. Das Programm funktioniert auf die gleiche Weise, unabhängig davon, in welchem ​​Status sich die Dateien befinden). In letzterem Fall müssen Sie jedoch den Cube und Ihre Programmimplementierung modellieren, wie Operationen an diesem Cube seinen Status ändern.


Als ich anti-funktionaler war, fragte ich mich, wie es gut sein könnte, wenn so etwas wie eine Festplatte veränderlich ist. Meine c # -Klassen hatten alle einen veränderlichen Status und konnten eine Festplatte oder ein anderes Gerät sehr logisch simulieren. Während bei der Funktionsweise ein Missverhältnis zwischen den Modellen und den tatsächlichen Maschinen bestand, die sie modellierten. Nachdem ich mich weiter mit der Funktionsweise befasst habe, habe ich festgestellt, dass die Vorteile, die Sie erhalten, dieses Problem ein gutes Stück überwiegen können. Und wenn es physikalisch möglich wäre, eine Festplatte zu erfinden, die eine Kopie von sich selbst erstellt, wäre dies tatsächlich nützlich (wie es das Journaling bereits tut).
LegendLength

5

Denken Sie zusätzlich zu den großartigen Antworten, die andere geben, an die Klassen Integerund Stringan Java. Instanzen dieser Klassen sind unveränderlich, aber das macht die Klassen nicht unbrauchbar, nur weil ihre Instanzen nicht geändert werden können. Die Unveränderlichkeit gibt Ihnen etwas Sicherheit. Sie wissen, wenn Sie eine String- oder Integer-Instanz als Schlüssel für a verwenden Map, kann der Schlüssel nicht geändert werden. Vergleichen Sie dies mit der DateKlasse in Java:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Sie haben stillschweigend einen Schlüssel in Ihrer Karte geändert! Die Arbeit mit unveränderlichen Objekten, wie beispielsweise in der funktionalen Programmierung, ist viel sauberer. Es ist einfacher zu überlegen, welche Nebenwirkungen auftreten - keine! Dies bedeutet, dass es für den Programmierer und auch für den Optimierer einfacher ist.


2
Ich verstehe das, aber es beantwortet meine Frage nicht. Denken Sie daran, dass ein Computerprogramm ein Modell eines realen Ereignisses oder Prozesses ist. Wenn Sie Ihre Werte nicht ändern können, wie modellieren Sie dann etwas, das sich ändert?
Mason Wheeler

Mit den Klassen Integer und String können Sie sicherlich nützliche Dinge tun. Es ist nicht so, dass ihre Unveränderlichkeit bedeutet, dass Sie keinen veränderlichen Zustand haben können.
Eddie

@ Mason Wheeler - Wenn man versteht, dass ein Ding und sein Zustand zwei verschiedene "Dinge" sind. Was Pacman ist, ändert sich nicht von Zeit A zu Zeit B. Wo Pacman ist, ändert sich. Wenn Sie von Zeit A zu Zeit B wechseln, erhalten Sie eine neue Kombination aus Pacman + Status ... das ist der gleiche Pacman, ein anderer Status. Nicht geänderter Zustand ... anderer Zustand.
RHSeeger

4

Für hoch interaktive Anwendungen wie Spiele ist Functional Reactive Programming Ihr Freund: Wenn Sie die Eigenschaften der Spielwelt als zeitlich variierende Werte (und / oder Ereignisströme) formulieren können , sind Sie bereit! Diese Formeln sind manchmal noch natürlicher und aufschlussreicher als das Mutieren eines Zustands, z. B. für einen sich bewegenden Ball können Sie direkt das bekannte Gesetz x = v * t verwenden . Und was ist besser, geschrieben die Spielregeln so compose besser als objektorientierte Abstraktionen. In diesem Fall kann die Geschwindigkeit des Balls beispielsweise auch ein zeitlich variierender Wert sein, der vom Ereignisstrom abhängt, der aus den Kollisionen des Balls besteht. Weitere konkrete Überlegungen zum Design finden Sie unter Erstellen von Spielen in Elm .


4

3

So würde FORTRAN ohne GEMEINSAME Blöcke funktionieren: Sie würden Methoden schreiben, die die von Ihnen übergebenen Werte und lokale Variablen enthalten. Das ist es.

Die objektorientierte Programmierung brachte uns Zustand und Verhalten zusammen, aber es war eine neue Idee, als ich sie 1994 zum ersten Mal in C ++ entdeckte.

Meine Güte, ich war ein funktionaler Programmierer, als ich Maschinenbauingenieur war und ich wusste es nicht!


2
Ich würde nicht zustimmen, dass dies etwas ist, das Sie an OO anheften können. Sprachen vor OO förderten den Kopplungsstatus und die Algorithmen. OO bot nur eine bessere Möglichkeit, dies zu verwalten.
Jason Baker

"Ermutigt" - vielleicht. OO machen es zu einem expliziten Teil der Sprache. Sie können Kapselungen und Informationen in C verstecken, aber ich würde sagen, dass OO-Sprachen es viel einfacher machen.
Duffymo

2

Denken Sie daran: Funktionssprachen sind Turing vollständig. Daher kann jede nützliche Aufgabe, die Sie in einer imperitiven Sprache ausführen würden, in einer funktionalen Sprache ausgeführt werden. Letztendlich denke ich jedoch, dass von einem hybriden Ansatz etwas zu sagen ist. Sprachen wie F # und Clojure (und ich bin sicher, andere) fördern das zustandslose Design, ermöglichen jedoch bei Bedarf die Veränderbarkeit.


Nur weil zwei Sprachen vollständig sind, heißt das nicht, dass sie dieselben Aufgaben ausführen können. Dies bedeutet, dass sie dieselbe Berechnung durchführen können. Brainfuck ist Turing vollständig, aber ich bin ziemlich sicher, dass es nicht über einen TCP-Stack kommunizieren kann.
RHSeeger

2
Sicher kann es. Bei gleichem Zugriff auf Hardware wie beispielsweise C ist dies möglich. Das bedeutet nicht, dass es praktisch wäre, aber die Möglichkeit ist da.
Jason Baker

2

Sie können keine reine funktionale Sprache haben, die nützlich ist. Es wird immer ein Maß an Veränderlichkeit geben, mit dem Sie sich befassen müssen. IO ist ein Beispiel.

Stellen Sie sich funktionale Sprachen als ein weiteres Werkzeug vor, das Sie verwenden. Es ist gut für bestimmte Dinge, aber nicht für andere. Das von Ihnen angegebene Spielbeispiel ist möglicherweise nicht der beste Weg, eine funktionale Sprache zu verwenden. Zumindest hat der Bildschirm einen veränderlichen Status, gegen den Sie mit FP nichts ändern können. Die Art und Weise, wie Sie über Probleme denken und welche Probleme Sie mit FP lösen, unterscheidet sich von denen, die Sie bei der imperativen Programmierung gewohnt sind.



-3

Das ist sehr einfach. Sie können in der Funktionsprogrammierung so viele Variablen verwenden, wie Sie möchten ... aber nur, wenn es sich um lokale Variablen handelt (die in Funktionen enthalten sind). Wickeln Sie also einfach Ihren Code in Funktionen ein, übergeben Sie Werte zwischen diesen Funktionen hin und her (als übergebene Parameter und zurückgegebene Werte) ... und das ist alles, was dazu gehört!

Hier ist ein Beispiel:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

John, welche Sprache ist das?
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.