你怎么能做任何有用的没有可变状态?

最近我一直在阅读很多关于函数式编程的东西,而且我可以理解它的大部分内容,但是我无法包揽头部的一件事就是无状态编码。 在我看来,通过消除可变状态来简化编程就像是通过移除仪表板来“简化”一辆汽车:成品可能更简单,但运气好让它与最终用户交互。

几乎所有我能想到的用户应用程序都将状态作为核心概念。 如果您编写文档(或SO帖子),则状态会随着每个新输入而改变。 或者如果你玩视频游戏,那么就会有很多状态变量,从所有角色的位置开始,这些角色会不断移动。 如何在不记录变化的价值的情况下做到有用的事情?

每次我找到讨论这个问题的东西时,它都会写入真正的技术功能 - 它会假设我没有的大量FP背景。 有没有人知道一种方式来解释这一点:对命令式编码有良好而坚实的理解的人,但在功能方面谁是完整的n00b?

编辑:一堆迄今为止的答复似乎试图说服我不变值的优势。 我得到那部分。 它非常有意义。 我不明白的是如何跟踪必须改变的值,并且不断变化,而不会有可变变量。


或者如果你玩视频游戏,那么就会有很多状态变量,从所有角色的位置开始,这些角色会不断移动。 如何在不记录变化的价值的情况下做到有用的事情?

如果你有兴趣,下面是一系列描述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

没有必要创建和操作列表mutable。 几乎所有的数据结构都可以很容易地转换为它们的功能等价物。 我在这里写了一个页面,它提供了堆栈,队列,左边的堆,红黑树,懒惰列表的不可变实现。 没有一段代码包含任何可变状态。 为了“变异”一棵树,我创建了一个全新的节点,并创建了一个新节点 - 这非常有效,因为我不需要复制树中的每个节点,我可以在新的节点中重用旧节点树。

使用更有意义的示例,我还编写了完全无状态的SQL解析器(或者至少我的代码是无状态的,我不知道底层的lexing库是否是无状态的)。

无状态编程与有状态编程一样具有表现力和强大功能,只需要一些练习来训练自己开始无状态思考。 当然,“尽可能无状态编程,必要时有状态编程”似乎是大多数不纯功能语言的座右铭。 当功能性方法不够干净或有效时,回退可变性没有任何坏处。


简答:你不能。

那么,关于不变性的大惊小怪呢?

如果你熟悉命令式语言,那么你知道“全局变差”。 为什么? 因为它们会在代码中引入(或有可能引入)一些非常难以解决的依赖关系。 依赖关系并不好; 你希望你的代码是模块化的。 程序的一部分不会影响其他部分尽可能少。 FP带给你模块化的圣杯:完全没有副作用。 你只需要你的f(x)= y。 把x放进去,让出来。 没有改变x或其他任何东西。 FP让你停止思考状态,并开始思考价值观。 你所有的功能只是接收值并产生新的值。

这有几个优点。

首先,没有副作用意味着更简单的程序,更容易推理。 不用担心引入新的程序部分会干扰和破坏现有的工作部分。

其次,这使程序平行化(高效的并行化是另一回事)。

第三,有一些可能的性能优势。 假设你有一个功能:

double x = 2 * x

现在你输入一个3的值,你得到一个6的值。 每次。 但是你也可以做到这一点,对吗? 是的。 但问题是,在必要的时候,你可以做更多。 我可以:

int y = 2;
int double(x){ return x * y; }

但我也可以这样做

int y = 2;
int double(x){ return x * (y++); }

命令式编译器不知道我是否会产生副作用,这使得优化变得更加困难(也就是说double 2不必每次都是4)。 功能的人知道我不会 - 因此,它可以优化每次看到“双2”。

现在,即使每次创建新值对于计算机内存方面的复杂类型的值来说都是非常浪费的,但并不一定如此。 因为如果你有f(x)= y,并且x和y的值“大部分是相同的”(例如只有几片叶子不同的树),那么x和y可以共享部分内存 - 因为它们都不会变异。

所以如果这个不可改变的事情如此之大,为什么我回答说,如果没有可变状态,你就无法做任何有用的事情。 好吧,没有可变性,你的整个程序将是一个巨大的f(x)= y函数。 对于程序的所有部分也是如此:只是功能,而功能则以“纯粹”的意思表示。 正如我所说的,这意味着每次f(x)= y。 因此,例如readFile(“myFile.txt”)每次都需要返回相同的字符串值。 不太有用。

因此,每个FP提供了一些变异状态的手段。 “纯”功能语言(例如Haskell)使用monad等有些可怕的概念来做到这一点,而“不纯”的(如ML)则直接允许这种语言。

当然,函数式语言还有许多其他的功能,使编程更高效,比如一流的功能等等。


请注意,说功能性编程没有'状态'有点误导性,可能是造成混淆的原因。 它绝对没有'可变状态',但它仍然可以有被操纵的值; 他们只是不能在原地进行更改(例如,您必须从旧值创建新值)。

这是一个粗略的过度简化,但想象你有一个OO语言,其中类的所有属性只在构造函数中设置一次,所有方法都是静态函数。 通过让方法获取包含它们计算所需的所有值的对象,然后返回带有结果的新对象(甚至可能是同一对象的新实例),仍然可以执行几乎任何计算。

将现有的代码翻译成这种模式可能是“困难的”,但那是因为它确实需要一种完全不同的代码思维方式。 尽管在大多数情况下,您可以免费获得很多并行机会。

附录:(关于如何跟踪需要改变的值的编辑)
当然,它们将被存储在一个不可变的数据结构中。

这不是一个建议的'解决方案',但最简单的方法是看到这将始终有效,因为您可以将这些不可变的值存储到类似结构的映射(字典/散列表)中,并以'变量名'为键。

很明显,在实际的解决方案中,你会使用更为理智的方法,但是这确实表明最糟糕的情况是,如果没有其他的工作,你可以用你在调用树中携带的地图“模拟”可变状态。

链接地址: http://www.djcxy.com/p/14187.html

上一篇: How can you do anything useful without mutable state?

下一篇: Advantages of stateless programming?