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

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

每次我发现一些讨论这个问题的东西,它都是用真正的技术函数语言写的,假设我没有浓厚的FP背景。有谁知道如何向那些对命令式编码有很好的、扎实的理解,但在函数方面完全是n00b的人解释这一点吗?

编辑:到目前为止,一堆回复似乎试图让我相信不可变值的优点。我懂你的意思。这很有道理。我不明白的是,在没有可变变量的情况下,如何跟踪必须不断变化的值。


当前回答

下面是如何在没有可变状态的情况下编写代码:不是将变化状态放入可变变量中,而是将其放入函数的参数中。不写循环,而是写递归函数。比如这段命令式代码:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

变成这样的函数代码(类似scheme的语法):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

或者这个Haskellish代码

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

至于为什么函数式程序员喜欢这样做(你没有问),你的程序中无状态的部分越多,就有越多的方法可以在不中断的情况下将这些部分组合在一起。无状态范式的强大之处在于它本身不具有无状态性(或纯粹性),而在于它使您能够编写强大的、可重用的函数并将它们组合起来。

你可以在John Hughes的论文Why Functional Programming Matters中找到一个很好的教程,里面有很多例子。

其他回答

让我们来回答更普遍的问题:

没有状态,你怎么做有用的事情?

你不。

寻找传统语言的替代品 我们必须首先认识到,一种制度不可能成为历史 敏感(允许执行一个程序来影响 一个后续的行为),除非系统已经 某种状态(第一个程序可以改变这种状态) 而第二个可以访问)。因此,历史敏感 计算系统的模型必须具有状态转换 语义学,至少在这个弱意义上。 约翰·巴克斯。

(由我强调)

重要的是巴克斯随后的观察:

但这不是 意味着每个计算都必须严重依赖于 复杂状态[…]

像Haskell或Clean这样的函数式语言允许您轻松地将这种观察付诸实践:大多数定义都是普通的函数,就像您在数学教育中看到的那样。这就留下了一小群“杂七杂八的人”来处理所有恼人的外部状态,例如:

与用户互动, 与远程服务通信, 处理模拟使用随机抽样, 打印出一个SVG文件(例如海报), 定期备份,

...两种语言都使用类型来区分普通和混杂。


有时,如果您试图实现的算法使用私有、局部可变状态实现,则效果最好。在这种情况下,你可以使用Haskell扩展来做到这一点,而不会让整个程序变得“内部杂乱”——详情请参阅John Launchbury和Simon Peyton Jones编写的State In Haskell。

通过使用大量的递归。

用f#(一种函数式语言)玩地字游戏。

Functional programming avoids state and emphasizes functionality. There's never any such thing as no state, though the state might actually be something that's immutable or baked into the architecture of what you're working with. Consider the difference between a static web server that just loads up files off the filesystem versus a program that implements a Rubik's cube. The former is going to be implemented in terms of functions designed to turn a request into a file path request into a response from the contents of that file. Virtually no state is needed beyond a tiny bit of configuration (the filesystem 'state' is really outside the scope of the program. The program works the same way regardless of what state the files are in). In the latter though, you need to model the cube and your program implementation of how operations on that cube change its state.

TLDR:你可以在没有可变状态的情况下进行任何计算,但是当真正需要告诉计算机该做什么的时候,因为计算机只使用可变状态,你需要在某些时候改变一些东西。

有很多答案正确地说,没有可变状态就不能做任何有用的事情,我想用一些简单的(反)例子来支持这一点,以及一个普遍的直觉。

如果你看到任何一段被认为是“纯函数式”的代码,并且它是这样做的(不是真正的语言):

printUpToTen = map println [1..10]

这不是纯功能性的。有一个隐藏状态(stdout的状态)不仅被改变了,而且隐式地传递进来。看起来像这样的代码(同样不是真正的语言):

printUpToTen = map println stdout [1..10]

也是不纯的:即使显式地传入state (stdout),它仍然是隐式突变的。

现在直观地说:可变状态是必要的,因为影响我们计算机的核心构建块是可变状态,这个构建块是内存。我们不能强迫计算机做任何事情,而不以某种方式操纵内存,即使我们的计算模型确实可以计算任何东西,而没有“内存”的概念。

Think of something like an old GameBoy Advance: in order to display something to the screen, you must modify the memory (there are certain addresses that are read many times a second that determine whats being put on the screen). Your computational model (pure functional programming) may not need state to operate, you may even be able to implement you model using an imperative, state manipulation model (like assembly) that abstracts the state manipulation, but at the end of the day, somewhere in you code you have to modify those addresses in memory for the device to actually display anything.

这就是命令式模型具有天然优势的地方:因为它们总是在操作状态,所以您可以非常容易地将其转换为实际修改内存。下面是一个渲染循环的例子:

while (1) {
   render(graphics_state);
}

如果你要展开循环,它看起来像这样:

render(graphics_state); // modified the memory
render(graphics_state); // modified the memory
render(graphics_state); // modified the memory
...

但在纯函数式语言中,你可能会得到这样的东西:

render_forever state = render_forever newState
    where newState = render state

展开(准确地说是压平)可以像这样可视化:

render(render(render(render(...state) // when is the memory actually changing??

// or if you want to expand it the other direction
...der(render(render(render(render(render(state) // no mutation

正如你所看到的,我们在状态上一遍又一遍地调用一个函数,状态是不断变化的,但我们从不改变内存:我们立即将它传递给下一个函数调用。即使我们的实现实际上在底层修改了一些表示状态的东西(甚至在适当的位置!),它也不在正确的位置。在某些时候,我们需要暂停并修改内存中的正确地址,这涉及到突变。

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

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

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