Lub jeśli grasz w grę wideo, istnieje mnóstwo zmiennych stanu, zaczynając od pozycji wszystkich postaci, które mają tendencję do ciągłego poruszania się. Jak możesz zrobić coś przydatnego bez śledzenia zmian wartości?
Jeśli jesteś zainteresowany, oto seria artykułów opisujących programowanie gier za pomocą Erlanga.
Prawdopodobnie nie spodoba ci się ta odpowiedź, ale nie dostaniesz funkcjonalnego programu, dopóki go nie użyjesz. Mogę wysłać próbki kodu i powiedzieć „Tutaj, nie widzisz ” - ale jeśli nie rozumiesz składni i zasad leżących u podstaw, twoje oczy po prostu się gapią. Z twojego punktu widzenia wygląda na to, że robię to samo co język imperatywny, ale po prostu ustanawiam wszelkie granice, aby celowo utrudnić programowanie. Z mojego punktu widzenia po prostu doświadczasz paradoksu Blub .
Na początku byłem sceptyczny, ale kilka lat temu wskoczyłem na funkcjonalny pociąg programistyczny i zakochałem się w nim. Sztuką programowania funkcjonalnego jest umiejętność rozpoznawania wzorców, określonych przypisań zmiennych i przenoszenia stanu imperatywnego na stos. Na przykład pętla for staje się rekurencją:
// 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
Nie jest bardzo ładna, ale mamy ten sam efekt bez mutacji. Oczywiście, gdy tylko jest to możliwe, lubimy całkowicie zapętlać i po prostu je wyodrębnić:
// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)
Metoda Seq.iter wyliczy kolekcję i wywoła anonimową funkcję dla każdego elementu. Bardzo przydatny :)
Wiem, że drukowanie liczb nie jest imponujące. Możemy jednak zastosować to samo podejście do gier: przytrzymaj cały stan na stosie i utwórz nowy obiekt z naszymi zmianami w wywołaniu rekurencyjnym. W ten sposób każda klatka jest bezstanową migawką gry, przy czym każda klatka po prostu tworzy zupełnie nowy obiekt z pożądanymi zmianami dowolnych bezstanowych obiektów wymagających aktualizacji. Pseudokod tego może być:
// 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))
Wersje imperatywna i funkcjonalna są identyczne, ale wersja funkcjonalna wyraźnie nie wykorzystuje stanu zmiennego. Kod funkcjonalny utrzymuje cały stan na stosie - miłą rzeczą w tym podejściu jest to, że jeśli coś pójdzie nie tak, debugowanie jest łatwe, wszystko czego potrzebujesz to ślad stosu.
Skaluje się do dowolnej liczby obiektów w grze, ponieważ wszystkie obiekty (lub kolekcje powiązanych obiektów) mogą być renderowane we własnym wątku.
Prawie każda aplikacja użytkownika, o której myślę, zawiera stan jako podstawową koncepcję.
W językach funkcjonalnych zamiast mutować stan obiektów, po prostu zwracamy nowy obiekt z pożądanymi zmianami. Jest bardziej wydajny niż się wydaje. Na przykład struktury danych są bardzo łatwe do przedstawienia jako niezmienne struktury danych. Na przykład stosy są niezwykle łatwe do wdrożenia:
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);
}
}
}
Powyższy kod konstruuje dwie niezmienne listy, dołącza je razem, aby utworzyć nową listę, i dołącza wyniki. Nigdzie w aplikacji nie jest używany żaden stan zmienny. Wygląda trochę nieporęcznie, ale to tylko dlatego, że C # jest pełnym językiem. Oto równoważny program w 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
Nie wymaga modyfikacji w celu tworzenia i manipulowania listami. Prawie wszystkie struktury danych można łatwo przekonwertować na ich funkcjonalne odpowiedniki. Napisałem tutaj stronę , która zapewnia niezmienne implementacje stosów, kolejek, stosów lewaków, czerwono-czarnych drzew, leniwych list. Żaden fragment kodu nie zawiera żadnego stanu podlegającego zmianom. Aby „mutować” drzewo, tworzę zupełnie nowy z nowym węzłem, który chcę - jest to bardzo wydajne, ponieważ nie muszę robić kopii każdego węzła w drzewie, mogę ponownie użyć starych w moim nowym drzewo.
Korzystając z bardziej znaczącego przykładu, napisałem również ten parser SQL, który jest całkowicie bezstanowy (a przynajmniej mój kod jest bezstanowy, nie wiem, czy podstawowa biblioteka leksykalna jest bezstanowa).
Programowanie bezstanowe jest tak samo wyraziste i potężne, jak programowanie stanowe, wymaga jedynie odrobiny praktyki, aby nauczyć się, jak zacząć myśleć bezpaństwowo. Oczywiście „programowanie bezstanowe, gdy to możliwe, programowanie stanowe tam, gdzie to konieczne” wydaje się być mottem najbardziej nieczystych języków funkcjonalnych. Nie ma nic złego w spadaniu na rzeczy zmienne, gdy funkcjonalne podejście po prostu nie jest tak czyste ani wydajne.