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

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

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

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


当前回答

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.

其他回答

注意,说函数式编程没有“状态”有点误导,可能是造成混淆的原因。它肯定没有“可变状态”,但它仍然可以有被操纵的值;它们只是不能就地更改(例如,您必须从旧值创建新值)。

这是一个严重的过度简化,但是想象一下你有一个OO语言,其中类上的所有属性只在构造函数中设置一次,所有方法都是静态函数。您仍然可以通过让方法获取包含计算所需的所有值的对象,然后返回带有结果的新对象(甚至可能是同一对象的新实例)来执行几乎任何计算。

将现有代码转换为这种范式可能“很难”,但这是因为它确实需要一种完全不同的思考代码的方式。但作为一个副作用,在大多数情况下,您可以免费获得大量并行机会。

附录:(关于如何跟踪需要更改的值的编辑) 当然,它们会被存储在一个不可变的数据结构中……

这不是一个建议的“解决方案”,但最简单的方法是,你可以将这些不可变的值存储到一个类似map(字典/哈希表)的结构中,以“变量名”为键。

显然,在实际解决方案中,您应该使用更明智的方法,但这确实表明,如果其他方法都不起作用,那么在最坏情况下,您可以使用这样一个贯穿调用树的映射来“模拟”可变状态。

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

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

以及滚动演示:

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

和可视化:

XSLT 曼德尔布洛特

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

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

我觉得这里有点误会。纯函数式程序有状态。不同之处在于该状态是如何建模的。在纯函数式编程中,状态是由接受某个状态并返回下一个状态的函数操作的。然后通过将状态传递给纯函数序列来实现状态排序。

甚至全局可变状态也可以这样建模。例如,在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。

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

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中找到一个很好的教程,里面有很多例子。