或者,如果您玩电子游戏,则有很多状态变量,从所有角色的位置开始,这些角色往往会不断移动。在不跟踪值更改的情况下,您怎么可能做些有用的事情?
如果您有兴趣,这里有一系列描述Erlang游戏编程的文章。
您可能不喜欢这个答案,但是直到使用它,您都不会得到功能程序。我可以张贴代码示例并说“在这里,您看不到 ”-但是,如果您不了解语法和基本原理,那么您的眼睛就会蒙上眼睛。从您的角度来看,似乎我在做与命令式语言相同的事情,但是只是设置各种边界以有目的地使编程变得更加困难。我的观点是,您只是在经历Blub悖论。
一开始我很怀疑,但是几年前我跳入了函数式编程的火车,并爱上了它。函数式编程的诀窍是能够识别模式,特定的变量分配,并将命令状态移至堆栈。例如,for循环变为递归:
// 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
它不是很漂亮,但是我们得到了相同的效果而没有突变。当然,只要有可能,我们都希望避免完全循环,而只需将其抽象掉即可:
// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)
Seq.iter方法将枚举整个集合,并为每个项目调用匿名函数。非常便利 :)
我知道,打印数字并不十分令人印象深刻。但是,我们可以对游戏使用相同的方法:将所有状态保留在堆栈中,并使用递归调用中的更改创建一个新对象。通过这种方式,每个帧都是游戏的无状态快照,其中,每个帧仅创建一个全新的对象,并对任何需要更新的无状态对象进行所需的更改。伪代码可能是:
// 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))
命令版本和功能版本相同,但是功能版本显然不使用任何可变状态。功能代码将所有状态保持在堆栈上-这种方法的好处是,如果出现问题,调试很容易,您只需要堆栈跟踪即可。
这可以扩展到游戏中任意数量的对象,因为所有对象(或相关对象的集合)都可以在其自己的线程中呈现。
我能想到的几乎每个用户应用程序都将状态作为核心概念。
在功能语言中,我们无需更改对象的状态,而只是返回具有所需更改的新对象。它比听起来更有效。例如,数据结构非常容易表示为不变的数据结构。例如,众所周知,堆栈很容易实现:
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);
}
}
}
上面的代码构造了两个不可变列表,将它们附加在一起以创建一个新列表,然后附加结果。在应用程序中的任何地方都不会使用可变状态。它看起来有点笨重,但这仅仅是因为C#是一种冗长的语言。这是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
创建和操作列表没有可变的必要。几乎所有数据结构都可以轻松转换为它们的功能等效项。我在这里写了一个页面,该页面提供了堆栈,队列,左派堆,红黑树,惰性列表的不可变实现。没有一个代码片段包含任何可变状态。要“变异”一棵树,我要创建一个带有所需新节点的全新树-这非常有效,因为我不需要复制树中的每个节点,因此可以在新节点中重用旧节点树。
使用一个更重要的例子,我还编写了这个SQL解析器,它完全是无状态的(或者至少我的代码是无状态的,我不知道底层的词法库是否是无状态的)。
无状态编程与有状态编程一样具有表现力和功能,只需要进行一些练习就可以训练自己开始无状态思考。当然,“尽可能进行无状态编程,必要时进行有状态编程”似乎是大多数不纯函数语言的座右铭。当功能性方法不是那么干净或高效时,回落到可变对象上没有任何危害。