最近我读了很多关于函数式编程的东西,大部分我都能理解,但有一件事我就是搞不懂,那就是无状态编码。在我看来,通过删除可变状态来简化编程就像通过删除仪表盘来“简化”一辆汽车:最终产品可能更简单,但希望它能与最终用户交互。

几乎我能想到的每个用户应用程序都将状态作为核心概念。如果你写了一个文档(或一个SO post),状态会随着每一个新的输入而改变。或者如果你玩电子游戏,会有大量的状态变量,从所有角色的位置开始,这些角色往往会不断移动。如果不跟踪不断变化的值,您怎么可能做任何有用的事情呢?

每次我发现一些讨论这个问题的东西,它都是用真正的技术函数语言写的,假设我没有浓厚的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

No mutable necessary to create and manipulate lists. Nearly all data structures can be easily converted into their functional equivalents. I wrote a page here which provides immutable implementations of stacks, queues, leftist heaps, red-black trees, lazy lists. Not a single snippet of code contains any mutable state. To "mutate" a tree, I create a brand new one with new node I want -- this is very efficient because I don't need to make a copy of every node in the tree, I can reuse the old ones in my new tree.

使用一个更重要的例子,我还编写了这个完全无状态的SQL解析器(或者至少我的代码是无状态的,我不知道底层词法库是否是无状态的)。

无状态编程与有状态编程一样具有表现力和强大的功能,它只需要一点点练习来训练自己开始无状态思考。当然,“尽可能使用无状态编程,必要时使用有状态编程”似乎是大多数非纯函数式语言的座右铭。当函数式方法不那么干净或有效时,求助于变量并没有什么坏处。

其他回答

请记住:函数式语言是图灵完备的。因此,任何用命令式语言执行的有用任务都可以用函数式语言完成。最后,我认为混合方法是有意义的。像f#和Clojure(我确信还有其他语言)这样的语言鼓励无状态设计,但在必要时允许可变性。

除了别人给出的很好的答案,想想Java中的Integer和String类。这些类的实例是不可变的,但这并不意味着仅仅因为它们的实例不可更改,这些类就毫无用处。不可变性给了你一定的安全性。您知道,如果使用String或Integer实例作为Map的键,则该键不能更改。将其与Java中的Date类进行比较:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

你已经无声地改变了地图中的一个键!使用不可变对象(如在函数式编程中)要干净得多。更容易推断会发生什么副作用——没有!这意味着程序员更容易,优化器也更容易。

JavaScript provides very clear examples of the different ways of approaching mutable or immutable state\values within its core because the ECMAScript specifications were not able to settle on a universal standard so one must continue to memorize or doublecheck which functions create a new object that they return or modify the original object passed to it. If your entire language is immutable then you know you are always getting a new (copied & possibly modified) result and never have to worry about accidentally modifying the variable before passing it into a function.

你知道哪个会返回一个新的对象,哪个会改变下面例子中的原始对象吗?

Array.prototype.push()
String.prototype.slice()
Array.prototype.splice()
String.prototype.trim()

For highly interactive applications such as games, Functional Reactive Programming is your friend: if you can formulate the properties of your game's world as time-varying values (and/or event streams), you are ready! These formulae will be sometimes even more natural and intent-revealing than mutating a state, e.g. for a moving ball, you can directly use the well-known law x = v * t. And what's better, the game's rules written such way compose better than object-oriented abstractions. For example, in this case, the ball's speed can be also a time-varying value, which depends on the event stream consisting of the ball's collisions. For more concrete design considerations, see Making Games in Elm.

简单的回答是:你不能。

那么不变性有什么好大惊小怪的呢?

If you're well-versed in imperative language, then you know that "globals are bad". Why? Because they introduce (or have the potential to introduce) some very hard-to-untangle dependencies in your code. And dependencies are not good; you want your code to be modular. Parts of program not influence other parts as little as possible. And FP brings you to the holy grail of modularity: no side effects at all. You just have your f(x) = y. Put x in, get y out. No changes to x or anything else. FP makes you stop thinking about state, and start thinking in terms of values. All of your functions simply receive values and produce new values.

这有几个优点。

首先,没有副作用意味着程序更简单,更容易推理。不用担心引入程序的新部分会干扰并使现有的正在工作的部分崩溃。

其次,这使得程序的可并行性微不足道(有效的并行化是另一回事)。

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

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)。函数函数知道我不会——因此,它可以在每次看到“double 2”时进行优化。

现在,即使每次创建新值对于复杂类型的值在计算机内存方面看起来是难以置信的浪费,但它不必如此。因为,如果你有f(x) = y,并且x和y的值“基本相同”(例如,只有少数叶子不同的树),那么x和y可以共享部分内存——因为它们都不会突变。

如果这个不可变的东西这么好,为什么我说没有可变状态就不能做任何有用的事情。如果没有可变性,整个程序就是一个巨大的f(x) = y函数。同样的道理也适用于程序的所有部分:只是函数,而且是“纯粹”意义上的函数。我说过,这意味着每次都是f(x) = y。因此,例如readFile("myFile.txt")每次都需要返回相同的字符串值。不是很有用。

因此,每个FP都提供了一些突变状态的方法。“纯”函数语言(例如Haskell)使用一些可怕的概念(如单子)来做到这一点,而“不纯”函数语言(例如ML)则直接允许这样做。

当然,函数式语言还带来了许多其他优点,使编程更加高效,比如一类函数等。