或者如果你玩电子游戏,有大量的状态变量,从所有角色的位置开始,他们往往会不断地四处走动。如果不跟踪值的变化,你怎么可能做任何有用的事情呢?
如果你有兴趣,这里有一系列描述 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 解析器(或者至少我的代码是无状态的,我不知道底层的词法库是否是无状态的)。
无状态编程与有状态编程一样富有表现力和强大,它只需要一点练习来训练自己开始无状态思考。当然,“尽可能无状态编程,必要时有状态编程”似乎是大多数不纯函数式语言的座右铭。当函数式方法不那么干净或高效时,使用可变变量并没有什么坏处。