Ou si vous jouez à un jeu vidéo, il y a des tonnes de variables d'état, en commençant par les positions de tous les personnages, qui ont tendance à se déplacer constamment. Comment pouvez-vous faire quelque chose d'utile sans suivre l'évolution des valeurs?
Si vous êtes intéressé, voici une série d'articles qui décrivent la programmation de jeux avec Erlang.
Vous ne serez probablement pas comme cette réponse, mais vous n'obtenir programme fonctionnel jusqu'à ce que vous l' utilisez. Je peux poster des exemples de code et dire "Ici, ne voyez- vous pas " - mais si vous ne comprenez pas la syntaxe et les principes sous-jacents, alors vos yeux se glacent. De votre point de vue, il semble que je fasse la même chose qu'un langage impératif, mais simplement en établissant toutes sortes de frontières pour rendre la programmation plus difficile. Mon point de vue, vous vivez juste le paradoxe Blub .
J'étais sceptique au début, mais j'ai sauté dans le train de la programmation fonctionnelle il y a quelques années et j'en suis tombé amoureux. L'astuce avec la programmation fonctionnelle est de pouvoir reconnaître des modèles, des affectations de variables particulières et déplacer l'état impératif vers la pile. Une boucle for, par exemple, devient récursive:
// 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
Ce n'est pas très joli, mais nous avons eu le même effet sans mutation. Bien sûr, dans la mesure du possible, nous aimons éviter de boucler complètement et simplement l'abstraire:
// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)
La méthode Seq.iter énumérera la collection et invoquera la fonction anonyme pour chaque élément. Très utile :)
Je sais, l'impression des chiffres n'est pas vraiment impressionnante. Cependant, nous pouvons utiliser la même approche avec les jeux: maintenir tous les états dans la pile et créer un nouvel objet avec nos modifications dans l'appel récursif. De cette façon, chaque image est un instantané sans état du jeu, où chaque image crée simplement un nouvel objet avec les changements souhaités de tout objet sans état qui doit être mis à jour. Le pseudocode pour cela pourrait ê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))
Les versions impératives et fonctionnelles sont identiques, mais la version fonctionnelle n'utilise clairement aucun état mutable. Le code fonctionnel conserve tout l'état sur la pile - la bonne chose à propos de cette approche est que, si quelque chose ne va pas, le débogage est facile, tout ce dont vous avez besoin est une trace de pile.
Cela augmente jusqu'à n'importe quel nombre d'objets dans le jeu, car tous les objets (ou collections d'objets associés) peuvent être rendus dans leur propre thread.
Presque toutes les applications utilisateur auxquelles je peux penser impliquent l’état comme concept de base.
Dans les langages fonctionnels, plutôt que de muter l'état des objets, nous renvoyons simplement un nouvel objet avec les changements que nous voulons. C'est plus efficace qu'il n'y paraît. Les structures de données, par exemple, sont très faciles à représenter comme des structures de données immuables. Les piles, par exemple, sont notoirement 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 construit deux listes immuables, les ajoute ensemble pour créer une nouvelle liste et ajoute les résultats. Aucun état modifiable n'est utilisé dans l'application. Il semble un peu volumineux, mais c'est uniquement parce que C # est un langage verbeux. 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
Aucun mutable nécessaire pour créer et manipuler des listes. Presque toutes les structures de données peuvent être facilement converties en leurs équivalents fonctionnels. J'ai écrit une page ici qui fournit des implémentations immuables de piles, files d'attente, tas de gauche, arbres rouge-noir, listes paresseuses. Pas un seul extrait de code ne contient d'état mutable. Pour "muter" un arbre, j'en crée un tout nouveau avec le 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 de l'arbre, je peux réutiliser les anciens dans mon nouveau arbre.
En utilisant un exemple plus significatif, j'ai également écrit cet analyseur SQL qui est totalement sans état (ou du moins mon code est sans état, je ne sais pas si la bibliothèque de lexing sous-jacente est sans état).
La programmation sans état est tout aussi expressive et puissante que la programmation avec état, elle nécessite juste un peu de pratique pour vous entraîner à commencer à penser sans état. Bien sûr, "la programmation sans état lorsque cela est possible, la programmation avec état si nécessaire" semble être la devise de la plupart des langages fonctionnels impurs. Il n'y a aucun mal à se rabattre sur les mutables lorsque l'approche fonctionnelle n'est tout simplement pas aussi propre ou efficace.