Ou si vous jouez à un jeu vidéo, il y a
des tonnes de variables d'état, début
avec la position de tous les
les personnages, qui ont tendance à se déplacer
sans cesse. Comment pouvez-vous faire
quelque chose d'utile sans conserver la trace
de l'évolution des valeurs?
Si vous êtes intéressés, voici une série d'articles qui décrivent la programmation de jeu avec Erlang.
Vous n'aurez probablement pas comme cette réponse, mais vous ne pouvez pas obtenir programme fonctionnel jusqu'à ce que vous l'utilisez. Je peux poster des exemples de code et de dire "ici, de ne pas vous voir", mais si vous ne comprenez pas la syntaxe et les principes sous-jacents, alors vous êtes yeux écarquillés. De votre point de vue, il semble que si je suis en train de faire la même chose comme un langage impératif, mais que de mettre en place toutes sortes de limites à bon escient de rendre la programmation plus difficile. De mon point de vue, vous êtes juste à l'expérience de la Blub paradoxe.
J'étais sceptique au début, mais j'ai sauté sur la programmation fonctionnelle en train il y a quelques années et est tombé en amour avec elle. Le truc avec la programmation fonctionnelle est d'être capable de reconnaître des modèles, notamment des assignations de variables, et de déplacer l'impératif de l'état de la pile. Une boucle for, par exemple, devient la récursivité:
// 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
Son pas très jolie, mais nous avons eu le même effet avec aucune mutation. Bien sûr, dans la mesure du possible, nous aimerions éviter la boucle complètement et simplement abstraction de loin:
// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)
Le Seq.iter méthode d'énumérer la collecte et appeler la fonction anonyme pour chaque élément. Très pratique :)
Je sais, l'impression des numéros n'est pas exactement pale. Cependant, nous pouvons utiliser la même approche avec les jeux: tenez tout l'état de la pile et de créer un nouvel objet avec les changements dans l'appel récursif. De cette façon, chaque image est un apatride instantané du jeu, où chaque image crée simplement une nouvelle marque de l'objet avec les modifications de votre choix quelle que soit apatrides objets besoins de mise à jour. Le pseudo-code de cette peut-être:
// 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))
L'impératif et fonctionnel versions sont identiques, mais la version fonctionnelle clairement utilise pas mutable état. Le code fonctionnel conserve tout état est tenu sur la pile -- la bonne chose à propos de cette approche est que, si quelque chose va mal, le débogage est facile, tout ce que vous avez besoin est une trace de la pile.
Ce s'adapte à n'importe quel nombre d'objets dans le jeu, parce que tous les objets (ou des collections d'objets liés) peuvent être rendus dans leur propre thread.
Juste au sujet de chaque utilisateur de l'application j'
pouvez penser implique l'état de base
concept.
Dans les langages fonctionnels, plutôt que la mutation de l'état des objets, nous nous contentons de renvoyer un nouvel objet avec les changements que nous voulons. Son plus efficace qu'il n'y paraît. Structures de données, par exemple, sont très faciles à représenter comme immuable des structures de données. Piles, par exemple, sont très faciles à mettre en œuvre:
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);
}
}
}
Le code ci-dessus, la construction de deux immuable listes, ajoute ensemble pour faire une nouvelle liste, et ajoute les résultats. Aucune mutable état est utilisé n'importe où dans l'application. Il semble un peu encombrant, mais c'est seulement parce que le C# est un bavard de la langue. Voici le programme équivalent en 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
Aucune mutable nécessaire de créer et de manipuler des listes. Presque toutes les structures de données peuvent être facilement convertis en équivalents fonctionnels. J'ai écrit une page ici qui fournit immuable implémentations de piles, files d'attente, de gauche tas d'arbres rouge-noir, paresseux listes. Pas un seul morceau de code contient tout mutable état. Pour "muter" un arbre, j'ai créer une nouvelle marque avec un nouveau nœud que je veux -- c'est très efficace, car je n'ai pas besoin de faire une copie de chaque nœud dans l'arbre, je peux réutiliser les ans dans mon arbre.
À l'aide d'un plus significatif, par exemple, j'ai aussi écrit cet analyseur SQL qui est totalement sans état (ou, au moins, mon code est apatride, je ne sais pas si le sous-jacent lexing bibliothèque est apatrides).
Apatrides, la programmation est tout aussi expressif et puissant comme dynamique de programmation, il faut juste un peu de pratique pour vous entraîner à commencer à penser statelessly. Bien sûr, les "apatrides de programmation lorsque cela est possible, la dynamique de la programmation si nécessaire" semble être la devise de la plupart des impurs langages fonctionnels. Il n'y a pas de mal à retomber sur des mutables lors de l'approche fonctionnelle n'est tout simplement pas aussi propre et efficace.