O se giochi a un videogioco, ci sono tonnellate di variabili di stato, a cominciare dalle posizioni di tutti i personaggi, che tendono a muoversi costantemente. Come puoi eventualmente fare qualcosa di utile senza tenere traccia della modifica dei valori?
Se sei interessato, ecco una serie di articoli che descrivono la programmazione del gioco con Erlang.
Probabilmente questa risposta non ti piacerà, ma non otterrai un programma funzionale fino a quando non lo utilizzerai. Posso inviare esempi di codice e dire "Qui, non vedi ", ma se non capisci la sintassi e i principi sottostanti, i tuoi occhi si fermano. Dal tuo punto di vista, sembra che stia facendo la stessa cosa di un linguaggio imperativo, ma sto solo impostando tutti i tipi di confini per rendere intenzionalmente più difficile la programmazione. Il mio punto di vista, stai solo vivendo il paradosso di Blub .
All'inizio ero scettico, ma qualche anno fa sono saltato sul treno di programmazione funzionale e me ne sono innamorato. Il trucco con la programmazione funzionale è riuscire a riconoscere schemi, particolari assegnazioni di variabili e spostare lo stato imperativo nello stack. Un for-loop, ad esempio, diventa ricorsione:
// 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
Non è molto carino, ma abbiamo ottenuto lo stesso effetto senza mutazione. Ovviamente, laddove possibile, ci piace evitare del tutto il looping e semplicemente astrarlo via:
// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)
Il metodo Seq.iter enumera la raccolta e invoca la funzione anonima per ogni articolo. Molto maneggevole :)
Lo so, la stampa dei numeri non è esattamente impressionante. Tuttavia, possiamo usare lo stesso approccio con i giochi: tenere tutto lo stato nello stack e creare un nuovo oggetto con le nostre modifiche nella chiamata ricorsiva. In questo modo, ogni fotogramma è un'istantanea senza stato del gioco, in cui ogni fotogramma crea semplicemente un oggetto nuovo di zecca con le modifiche desiderate di qualsiasi oggetto senza stato che necessita di essere aggiornato. Lo pseudocodice per questo potrebbe essere:
// 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))
Le versioni imperative e funzionali sono identiche, ma la versione funzionale chiaramente non utilizza uno stato mutabile. Il codice funzionale mantiene tutto lo stato presente nello stack - la cosa bella di questo approccio è che, se qualcosa va storto, il debug è facile, tutto ciò che serve è una traccia dello stack.
Questo aumenta fino a qualsiasi numero di oggetti nel gioco, poiché tutti gli oggetti (o raccolte di oggetti correlati) possono essere renderizzati nel proprio thread.
Quasi ogni applicazione utente che mi viene in mente coinvolge lo stato come un concetto chiave.
Nei linguaggi funzionali, anziché mutare lo stato degli oggetti, restituiamo semplicemente un nuovo oggetto con le modifiche che desideriamo. È più efficiente di quanto sembri. Le strutture dati, ad esempio, sono molto facili da rappresentare come strutture dati immutabili. Le pile, ad esempio, sono notoriamente facili da implementare:
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);
}
}
}
Il codice sopra crea due elenchi immutabili, li aggiunge insieme per creare un nuovo elenco e accoda i risultati. Nessuno stato mutabile viene utilizzato in nessun punto dell'applicazione. Sembra un po 'ingombrante, ma questo è solo perché C # è un linguaggio prolisso. Ecco il programma equivalente 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
Nessun mutevole necessario per creare e manipolare elenchi. Quasi tutte le strutture di dati possono essere facilmente convertite nei loro equivalenti funzionali. Ho scritto qui una pagina che fornisce implementazioni immutabili di pile, code, cumuli di sinistra, alberi rosso-neri, liste pigre. Non un singolo frammento di codice contiene alcuno stato modificabile. Per "mutare" un albero, ne creo uno nuovo con un nuovo nodo che voglio - questo è molto efficiente perché non ho bisogno di fare una copia di ogni nodo dell'albero, posso riutilizzare quelli vecchi nel mio nuovo albero.
Usando un esempio più significativo, ho anche scritto questo parser SQL che è totalmente senza stato (o almeno il mio codice è senza stato, non so se la libreria lexing sottostante sia senza stato).
La programmazione senza stato è altrettanto espressiva e potente della programmazione con stato, richiede solo un po 'di pratica per allenarsi per iniziare a pensare senza stato. Naturalmente, "programmazione senza stato quando possibile, programmazione con stato ove necessario" sembra essere il motto della maggior parte dei linguaggi funzionali impuri. Non c'è nulla di male nel ricadere sui mutabili quando l'approccio funzionale non è altrettanto pulito o efficiente.