O si juegas un videojuego, hay toneladas de variables de estado, comenzando con las posiciones de todos los personajes, que tienden a moverse constantemente. ¿Cómo puedes hacer algo útil sin hacer un seguimiento de los valores cambiantes?
Si estás interesado, aquí hay una serie de artículos que describen la programación de juegos con Erlang.
Probablemente no le guste esta respuesta, pero no obtendrá un programa funcional hasta que lo use. Puedo publicar ejemplos de código y decir "Aquí, no lo ves ", pero si no entiendes la sintaxis y los principios subyacentes, entonces tus ojos simplemente se nublan. Desde su punto de vista, parece que estoy haciendo lo mismo que un lenguaje imperativo, pero simplemente estableciendo todo tipo de límites para dificultar la programación a propósito. Mi punto de vista, solo estás experimentando la paradoja de Blub .
Al principio era escéptico, pero me subí al tren de programación funcional hace unos años y me enamoré de él. El truco con la programación funcional es poder reconocer patrones, asignaciones de variables particulares y mover el estado imperativo a la pila. Un ciclo for, por ejemplo, se convierte en recursividad:
// 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
No es muy bonito, pero obtuvimos el mismo efecto sin mutación. Por supuesto, siempre que sea posible, nos gusta evitar los bucles por completo y simplemente abstraerlo:
// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)
El método Seq.iter enumerará a través de la colección e invocará la función anónima para cada elemento. Muy útil :)
Lo sé, imprimir números no es exactamente impresionante. Sin embargo, podemos usar el mismo enfoque con los juegos: mantener todo el estado en la pila y crear un nuevo objeto con nuestros cambios en la llamada recursiva. De esta manera, cada cuadro es una instantánea sin estado del juego, donde cada cuadro simplemente crea un nuevo objeto con los cambios deseados de los objetos sin estado que necesitan actualizarse. El pseudocódigo para esto podría ser:
// 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))
Las versiones imperativas y funcionales son idénticas, pero la versión funcional claramente no utiliza un estado mutable. El código funcional mantiene todo el estado en la pila; lo bueno de este enfoque es que, si algo sale mal, la depuración es fácil, todo lo que necesita es un seguimiento de la pila.
Esto escala a cualquier número de objetos en el juego, porque todos los objetos (o colecciones de objetos relacionados) se pueden representar en su propio hilo.
Casi todas las aplicaciones de usuario que se me ocurren implican al estado como un concepto central.
En lenguajes funcionales, en lugar de mutar el estado de los objetos, simplemente devolvemos un nuevo objeto con los cambios que queremos. Es más eficiente de lo que parece. Las estructuras de datos, por ejemplo, son muy fáciles de representar como estructuras de datos inmutables. Las pilas, por ejemplo, son notoriamente fáciles de implementar:
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);
}
}
}
El código anterior construye dos listas inmutables, las agrega juntas para hacer una nueva lista y agrega los resultados. No se utiliza ningún estado mutable en ninguna parte de la aplicación. Parece un poco voluminoso, pero eso es solo porque C # es un lenguaje detallado. Aquí está el programa equivalente 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
No se necesita mutable para crear y manipular listas. Casi todas las estructuras de datos se pueden convertir fácilmente en sus equivalentes funcionales. Escribí una página aquí que proporciona implementaciones inmutables de pilas, colas, montones izquierdistas, árboles rojo-negros, listas perezosas. Ningún fragmento de código contiene ningún estado mutable. Para "mutar" un árbol, creo uno nuevo con el nuevo nodo que quiero. Esto es muy eficiente porque no necesito hacer una copia de cada nodo en el árbol, puedo reutilizar los viejos en mi nuevo árbol.
Usando un ejemplo más significativo, también escribí este analizador SQL que no tiene estado (o al menos mi código no tiene estado, no sé si la biblioteca subyacente no tiene estado).
La programación sin estado es tan expresiva y poderosa como la programación con estado, solo requiere un poco de práctica para entrenarse para comenzar a pensar sin estado. Por supuesto, "programación sin estado cuando sea posible, programación con estado cuando sea necesario" parece ser el lema de los lenguajes funcionales más impuros. No hay daño en recurrir a las mutables cuando el enfoque funcional no es tan limpio o eficiente.