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

几乎我能想到的每个用户应用程序都将状态作为核心概念。如果你写了一个文档(或一个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解析器(或者至少我的代码是无状态的,我不知道底层词法库是否是无状态的)。

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

其他回答

这很简单。在函数式编程中,你可以使用任意多的变量……但前提是它们是局部变量(包含在函数中)。因此,只需将代码包装在函数中,在这些函数之间来回传递值(作为传递的参数和返回值)……这就是它的全部!

这里有一个例子:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

只是做同一件事的不同方式。

Consider a simple example such as adding the numbers 3, 5, and 10. Imagine thinking about doing that by first changing the value of 3 by adding 5 to it, then adding 10 to that "3", then outputting the current value of "3" (18). This seems patently ridiculous, but it is in essence the way that state-based imperative programming is often done. Indeed, you can have many different "3"s that have the value 3, yet are different. All of this seems odd, because we have been so ingrained with the, quite enormously sensible, idea that the numbers are immutable.

现在,当您将值设置为不可变时,考虑添加3、5和10。3加5得到另一个值8,然后再加10得到另一个值18。

这是做同样事情的等价方法。所有必要的信息都存在于这两种方法中,只是形式不同。其中,信息以状态的形式存在,并存在于改变状态的规则中。在另一种情况下,信息存在于不可变数据和函数定义中。

事实上,即使在没有可变状态的语言中,也很容易有一些看起来像可变状态的东西。

Consider a function with type s -> (a, s). Translating from Haskell syntax, it means a function which takes one parameter of type "s" and returns a pair of values, of types "a" and "s". If s is the type of our state, this function takes one state and returns a new state, and possibly a value (you can always return "unit" aka (), which is sort of equivalent to "void" in C/C++, as the "a" type). If you chain several calls of functions with types like this (getting the state returned from one function and passing it to the next), you have "mutable" state (in fact you are in each function creating a new state and abandoning the old one).

It might be easier to understand if you imagine the mutable state as the "space" where your program is executing, and then think of the time dimension. At instant t1, the "space" is in a certain condition (say for example some memory location has value 5). At a later instant t2, it is in a different condition (for example that memory location now has value 10). Each of these time "slices" is a state, and it is immutable (you cannot go back in time to change them). So, from this point of view, you went from the full spacetime with a time arrow (your mutable state) to a set of slices of spacetime (several immutable states), and your program is just treating each slice as a value and computing each of them as a function applied to the previous one.

好吧,也许这并不容易理解:-)

It might seem inneficient to explicitly represent the whole program state as a value, which has to be created only to be discarded the next instant (just after a new one is created). For some algorithms it might be natural, but when it is not, there is another trick. Instead of a real state, you can use a fake state which is nothing more than a marker (let's call the type of this fake state State#). This fake state exists from the point of view of the language, and is passed like any other value, but the compiler completely omits it when generating the machine code. It only serves to mark the sequence of execution.

举个例子,假设编译器给了我们以下函数:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

从这些类似haskell的声明中转换,readRef接收到类似于指向“a”类型值和假状态的指针或句柄的东西,并返回由第一个形参和新的假状态指向的“a”类型值。writeRef类似,但是改变了所指向的值。

If you call readRef and then pass it the fake state returned by writeRef (perhaps with other calls to unrelated functions in the middle; these state values create a "chain" of function calls), it will return the value written. You can call writeRef again with the same pointer/handle and it will write to the same memory location — but, since conceptually it is returning a new (fake) state, the (fake) state is still imutable (a new one has been "created"). The compiler will call the functions in the order it would have to call them if there was a real state variable which had to be computed, but the only state which there is is the full (mutable) state of the real hardware.

(了解Haskell的人会注意到我简化了很多东西,并省略了一些重要的细节。对于那些想要看到更多细节的人,看看mtl中的Control.Monad.State,以及ST和IO(又名ST RealWorld)单子。

You might wonder why doing it in such a roundabout way (instead of simply having mutable state in the language). The real advantage is that you have reified your program's state. What before was implicit (your program state was global, allowing for things like action at a distance) is now explicit. Functions which do not receive and return the state cannot modify it or be influenced by it; they are "pure". Even better, you can have separate state threads, and with a bit of type magic, they can be used to embed an imperative computation within a pure one, without making it impure (the ST monad in Haskell is the one normally used for this trick; the State# I mentioned above is in fact GHC's State# s, used by its implementation of the ST and IO monads).

或者如果你玩电子游戏,有 大量的状态变量,开始 所有的位置 角色,他们倾向于移动 不断。你怎么可能呢 没有记录任何有用的东西 改变价值观?

如果您感兴趣,这里有一系列描述使用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解析器(或者至少我的代码是无状态的,我不知道底层词法库是否是无状态的)。

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

除了别人给出的很好的答案,想想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());

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