最近我读了很多关于函数式编程的东西,大部分我都能理解,但有一件事我就是搞不懂,那就是无状态编码。在我看来,通过删除可变状态来简化编程就像通过删除仪表盘来“简化”一辆汽车:最终产品可能更简单,但希望它能与最终用户交互。
几乎我能想到的每个用户应用程序都将状态作为核心概念。如果你写了一个文档(或一个SO post),状态会随着每一个新的输入而改变。或者如果你玩电子游戏,会有大量的状态变量,从所有角色的位置开始,这些角色往往会不断移动。如果不跟踪不断变化的值,您怎么可能做任何有用的事情呢?
每次我发现一些讨论这个问题的东西,它都是用真正的技术函数语言写的,假设我没有浓厚的FP背景。有谁知道如何向那些对命令式编码有很好的、扎实的理解,但在函数方面完全是n00b的人解释这一点吗?
编辑:到目前为止,一堆回复似乎试图让我相信不可变值的优点。我懂你的意思。这很有道理。我不明白的是,在没有可变变量的情况下,如何跟踪必须不断变化的值。
让我们来回答更普遍的问题:
没有状态,你怎么做有用的事情?
你不。
寻找传统语言的替代品
我们必须首先认识到,一种制度不可能成为历史
敏感(允许执行一个程序来影响
一个后续的行为),除非系统已经
某种状态(第一个程序可以改变这种状态)
而第二个可以访问)。因此,历史敏感
计算系统的模型必须具有状态转换
语义学,至少在这个弱意义上。
约翰·巴克斯。
(由我强调)
重要的是巴克斯随后的观察:
但这不是
意味着每个计算都必须严重依赖于
复杂状态[…]
像Haskell或Clean这样的函数式语言允许您轻松地将这种观察付诸实践:大多数定义都是普通的函数,就像您在数学教育中看到的那样。这就留下了一小群“杂七杂八的人”来处理所有恼人的外部状态,例如:
与用户互动,
与远程服务通信,
处理模拟使用随机抽样,
打印出一个SVG文件(例如海报),
定期备份,
...两种语言都使用类型来区分普通和混杂。
有时,如果您试图实现的算法使用私有、局部可变状态实现,则效果最好。在这种情况下,你可以使用Haskell扩展来做到这一点,而不会让整个程序变得“内部杂乱”——详情请参阅John Launchbury和Simon Peyton Jones编写的State In Haskell。
我觉得这里有点误会。纯函数式程序有状态。不同之处在于该状态是如何建模的。在纯函数式编程中,状态是由接受某个状态并返回下一个状态的函数操作的。然后通过将状态传递给纯函数序列来实现状态排序。
甚至全局可变状态也可以这样建模。例如,在Haskell中,程序是一个从World到World的函数。也就是说,你传入整个宇宙,程序返回一个新的宇宙。但是,在实践中,您只需要传入您的程序实际感兴趣的部分。程序实际上返回一系列动作,作为程序运行的操作环境的指令。
您希望从命令式编程的角度对此进行解释。好的,让我们看一些用函数式语言编写的非常简单的命令式编程。
考虑下面的代码:
int x = 1;
int y = x + 1;
x = x + y;
return x;
相当标准的命令式代码。没有做什么有趣的事情,但这是可以用来说明的。我想你们会同意这里涉及到国家。变量x的值随时间变化。现在,让我们稍微改变一下符号,发明一个新的语法:
let x = 1 in
let y = x + 1 in
let z = x + y in z
加上括号使它的意思更清楚:
let x = 1 in (let y = x + 1 in (let z = x + y in (z)))
可以看到,状态是由一系列纯表达式建模的,这些纯表达式绑定了下面表达式的自由变量。
您会发现这个模式可以建模任何类型的状态,甚至IO。
简单的回答是:你不能。
那么不变性有什么好大惊小怪的呢?
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)则直接允许这样做。
当然,函数式语言还带来了许多其他优点,使编程更加高效,比如一类函数等。