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.