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

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

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

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


当前回答

我现在才开始讨论这个问题,但是我想为那些正在与函数式编程作斗争的人补充几点。

函数式语言维护与命令式语言完全相同的状态更新,但它们是通过将更新后的状态传递给后续的函数调用来实现的。这是一个沿着数轴移动的简单例子。您的状态是您当前的位置。

首先是命令式方式(在伪代码中)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

现在是函数式的方式(在伪代码中)。我非常依赖三元运算符,因为我希望有命令式背景的人能够读懂这段代码。所以如果你不经常使用三元运算符(我总是避免它在我的命令式的日子)下面是它是如何工作的。

predicate ? if-true-expression : if-false-expression

您可以通过将一个新的三元表达式放在假表达式的位置来连接三元表达式

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

考虑到这一点,下面是函数版本。

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

这是一个简单的例子。如果这是在游戏世界中移动人,你就必须引入一些副作用,如在屏幕上绘制对象的当前位置,并根据对象移动的速度在每次调用中引入一些延迟。但你仍然不需要可变状态。

The lesson is that functional languages "mutate" state by calling the function with different parameters. Obviously this doesn't really mutate any variables, but that's how you get a similar effect. This means you'll have to get used to thinking recursively if you want to do functional programming. Learning to think recursively is not hard, but it does take both practice and a toolkit. That small section in that "Learn Java" book where they used recursion to calculate factorial does not cut it. You need a toolkit of skills like making iterative processes out of recursion (this is why tail recursion is essential for functional language), continuations, invariants, etc. You wouldn't do OO programming without learning about access modifiers, interfaces etc. Same thing for functional programming.

我的建议是做小Schemer(注意我说的是“做”而不是“读”),然后做SICP的所有练习。当你完成时,你的大脑会和刚开始时不一样。

其他回答

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.

简单的回答是:你不能。

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

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)则直接允许这样做。

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

使用一些创造性和模式匹配,无状态游戏被创造出来:

迷宫游戏 迷宫游戏2 CSSPlay:井字 纯CSS一字棋 CSSPlay:乒乓球 CSSPlay:乒乓球 CSSPlay:警察与强盗 CSSPlay: Whack-a-Rat CSS3 Pong:与CSS有关的疯狂事情

以及滚动演示:

随机英雄 动画模拟SVG时钟 动画SVG摆 动画SVG赛车手 CSS3:造雪

和可视化:

XSLT 曼德尔布洛特

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

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

你不。

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

(由我强调)

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

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

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

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

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


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

我现在才开始讨论这个问题,但是我想为那些正在与函数式编程作斗争的人补充几点。

函数式语言维护与命令式语言完全相同的状态更新,但它们是通过将更新后的状态传递给后续的函数调用来实现的。这是一个沿着数轴移动的简单例子。您的状态是您当前的位置。

首先是命令式方式(在伪代码中)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

现在是函数式的方式(在伪代码中)。我非常依赖三元运算符,因为我希望有命令式背景的人能够读懂这段代码。所以如果你不经常使用三元运算符(我总是避免它在我的命令式的日子)下面是它是如何工作的。

predicate ? if-true-expression : if-false-expression

您可以通过将一个新的三元表达式放在假表达式的位置来连接三元表达式

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

考虑到这一点,下面是函数版本。

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

这是一个简单的例子。如果这是在游戏世界中移动人,你就必须引入一些副作用,如在屏幕上绘制对象的当前位置,并根据对象移动的速度在每次调用中引入一些延迟。但你仍然不需要可变状态。

The lesson is that functional languages "mutate" state by calling the function with different parameters. Obviously this doesn't really mutate any variables, but that's how you get a similar effect. This means you'll have to get used to thinking recursively if you want to do functional programming. Learning to think recursively is not hard, but it does take both practice and a toolkit. That small section in that "Learn Java" book where they used recursion to calculate factorial does not cut it. You need a toolkit of skills like making iterative processes out of recursion (this is why tail recursion is essential for functional language), continuations, invariants, etc. You wouldn't do OO programming without learning about access modifiers, interfaces etc. Same thing for functional programming.

我的建议是做小Schemer(注意我说的是“做”而不是“读”),然后做SICP的所有练习。当你完成时,你的大脑会和刚开始时不一样。