没有可变状态,你怎么能做任何有用的事情?

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

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

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

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

59183 次浏览

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

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

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

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

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

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

这就是没有COMMON块的FORTRAN的工作方式:您将编写具有传入值和局部变量的方法。就是这样。

面向对象编程将我们的状态和行为结合在一起,但当我在1994年第一次从c++中接触到它时,它还是一个新思想。

天啊,当我还是机械工程师的时候,我是一个函数式程序员,而我却不知道!

函数式编程避免了状态和强调功能。从来没有没有状态这样的东西,尽管状态实际上可能是不可变的或嵌入到您正在使用的体系结构中的东西。考虑一下只从文件系统中加载文件的静态web服务器与实现魔方的程序之间的区别。前者将根据将请求转换为文件路径请求的函数来实现,该请求将转换为来自该文件内容的响应。实际上,除了少量的配置之外,不需要任何状态(文件系统的“状态”实际上超出了程序的范围。无论文件处于何种状态,该程序都以相同的方式工作)。但在后者中,您需要对多维数据集和程序实现建模,以了解对该多维数据集的操作如何改变其状态。

通过使用大量的递归。

f#(一种函数式语言。

简单的回答是:你不能。

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

如果您精通命令式语言,那么您就知道“全局变量是不好的”。为什么?因为它们在代码中引入了(或有可能引入)一些非常难以理清的依赖项。依赖关系不好;你希望你的代码是模块化。程序的各个部分尽量不影响其他部分。FP将你带到模块化的圣杯:没有副作用在所有。f(x) = y,代入x,代出y。x和其他东西没有变化。FP使您不再考虑状态,而是开始考虑值。所有函数都只是接收值并生成新值。

这有几个优点。

首先,没有副作用意味着程序更简单,更容易推理。不用担心引入程序的新部分会干扰并使现有的正在工作的部分崩溃。

其次,这使得程序的可并行性微不足道(有效的并行化是另一回事)。

第三,有一些可能的性能优势。假设你有一个函数:

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

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

除了别人给出的精彩答案,想想Java中的类IntegerString。这些类的实例是不可变的,但这并不意味着仅仅因为它们的实例不可更改,这些类就毫无用处。不可变性给了你一定的安全性。你知道,如果你使用一个String或Integer实例作为Map的键,这个键是不能改变的。将其与Java中的Date类进行比较:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

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

请记住:函数式语言是图灵完备的。因此,任何用命令式语言执行的有用任务都可以用函数式语言完成。最后,我认为混合方法是有意义的。像f#和Clojure(我确信还有其他语言)这样的语言鼓励无状态设计,但在必要时允许可变性。

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

考虑一个s -> (a, s)类型的函数。从Haskell语法翻译过来,它意味着一个函数接受一个类型为“s”的参数,并返回一对类型为“a”和“s”的值。如果s是我们的状态类型,这个函数接受一个状态并返回一个新状态,可能还有一个值(你总是可以返回“unit”,也就是(),它有点相当于C/ c++中的“void”,作为“a”类型)。如果您将这样类型的函数调用链接在一起(从一个函数获取返回的状态并将其传递给下一个函数),那么您就拥有“可变”状态(实际上您在每个函数中都创建了一个新状态并放弃了旧状态)。

如果您将可变状态想象为程序正在执行的“空间”,然后考虑时间维度,那么可能更容易理解。在瞬间t1,“空间”处于特定的状态(例如,某个内存位置的值为5)。在随后的瞬间t2,它处于不同的状态(例如,内存位置现在的值为10)。这些时间“切片”中的每一个都是一种状态,并且是不可变的(你不能回到过去改变它们)。因此,从这个角度来看,你从一个带时间箭头的完整时空(你的可变状态)到一组时空切片(几个不可变状态),你的程序只是将每个切片视为一个值,并将它们作为应用于前一个的函数来计算。

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

显式地将整个程序状态表示为一个值似乎是不够的,必须创建这个值,然后在下一个时刻(就在创建一个新值之后)丢弃它。对于某些算法来说,这可能是自然的,但如果不是,还有另一个技巧。你可以使用一个假状态,而不是一个真实的状态,它只不过是一个标记(让我们把这个假状态的类型称为State#)。从语言的角度来看,这个假状态是存在的,并且像其他值一样传递,但是编译器在生成机器代码时完全忽略它。它只是用来标记执行的顺序。

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

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

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

如果调用readRef,然后将writeRef返回的假状态传递给它(中间可能有对不相关函数的其他调用;这些状态值创建了一个函数调用的“链”),它将返回写入的值。你可以用相同的指针/句柄再次调用writeRef,它将写入相同的内存位置—但是,因为在概念上它返回了一个新的(假的)状态,所以(假的)状态仍然是不可变的(一个新的状态已经被“创建”了)。编译器会按照必须计算的真实状态变量的顺序调用函数,但唯一的状态是真实硬件的完整(可变)状态。

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

您可能想知道为什么要以这种迂回的方式(而不是简单地在语言中使用可变状态)来实现它。真正的优势是你有具体化你的程序的状态。以前是隐式的(你的程序状态是全局的,允许像在远处行动这样的东西)现在是显式的。不接收和返回状态的函数不能修改状态或受状态影响;他们是“纯粹的”。更好的是,你可以拥有独立的状态线程,并且使用一点类型魔法,它们可以被用来将命令式计算嵌入到纯计算中,而不会使其不纯(Haskell中的ST单子通常用于此技巧;我上面提到的State#实际上是GHC的State# s,由它的STIO单子的实现使用)。

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

考虑一个简单的例子,例如将数字3、5和10相加。想象一下这样做:首先改变3的值,给它加上5,然后给“3”加上10,然后输出当前值“3”(18)。这看起来显然很荒谬,但本质上这正是基于状态的命令式编程通常采用的方式。实际上,您可以有许多不同的值为3的“3”,但它们是不同的。所有这些看起来都很奇怪,因为我们已经根深蒂固地认为数字是不可改变的,这是非常明智的想法。

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

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

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

如果你感兴趣,这是系列文章描述了用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))

命令式版本和函数式版本是相同的,但是函数式版本显然没有使用可变状态。函数式代码将所有状态保存在堆栈上——这种方法的好处是,如果出现错误,调试很容易,您所需要的只是堆栈跟踪。

这可以扩展到游戏中任意数量的对象,因为所有对象(或相关对象的集合)都可以在自己的线程中呈现。

几乎每一个用户应用程序I 可以把国家看作一个核心吗 概念。< / p >

在函数式语言中,我们不是改变对象的状态,而是简单地返回一个带有我们想要的更改的新对象。它比听起来更有效率。例如,数据结构很容易表示为不可变数据结构。例如,堆栈是出了名的容易实现:

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

创建和操作列表不需要可变。几乎所有的数据结构都可以很容易地转换成它们的功能对等物。我写了一个页面在这里,它提供了栈、队列、左堆、红黑树、惰性列表的不可变实现。没有一个代码片段包含任何可变状态。为了“变异”一棵树,我用我想要的新节点创建一个全新的树——这非常有效,因为我不需要复制树中的每个节点,我可以在我的新树中重用旧的节点。

使用一个更重要的例子,我还编写了完全无状态的这个SQL解析器(或者至少我的代码是无状态的,我不知道底层词法库是否是无状态的)。

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

你不可能有一种有用的纯函数式语言。总会有一定程度的可变性需要处理,IO就是一个例子。

将函数式语言视为您使用的另一种工具。它对某些事情有好处,但对其他事情没有好处。你给出的游戏例子可能不是使用函数语言的最佳方式,至少屏幕会有一个可变的状态,你不能用FP做任何事情。使用FP思考问题的方式和解决问题的类型将不同于使用命令式编程所习惯的方式。

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

甚至全局可变状态也可以这样建模。例如,在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的论文为什么函数式编程很重要中找到一个很好的教程,里面有很多例子。

对于高度交互的应用程序,如游戏,函数式响应式编程是你的朋友:如果你可以将游戏世界的属性定义为时变的价值观(和/或事件流),你就准备好了!这些公式有时甚至比改变一个状态更自然,更能揭示意图,例如,对于一个移动的球,你可以直接使用众所周知的定律X = v * t。而且更好的是,游戏规则的编写方式组成比面向对象的抽象更好。例如,在这种情况下,球的速度也可以是时变值,这取决于由球的碰撞组成的事件流。有关更具体的设计考虑因素,请参见在Elm制作游戏

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

以及滚动演示:

和可视化:

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

这里有一个例子:

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()
)
);

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

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

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

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)
)

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

  1. 我们得到的教训是,函数式语言通过调用带有不同参数的函数来“改变”状态。显然这并没有改变任何变量,但这就是你得到类似效果的方式。这意味着如果您想进行函数式编程,就必须习惯递归思维。

  2. 学习递归思考并不难,但它确实需要练习和工具箱。在“学习Java”一书中,他们使用递归计算阶乘的那一小部分并没有删去它。你需要一个技能工具包,比如从递归中生成迭代过程(这就是为什么尾部递归对于函数式语言非常重要)、延续、不变量等等。如果不学习访问修饰符、接口等,就不会进行面向对象编程。函数式编程也是如此。

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

JavaScript提供了非常清晰的例子,说明了在其核心中处理可变或不可变状态值的不同方法,因为ECMAScript规范无法确定一个通用的标准,所以人们必须继续记忆或反复检查哪些函数创建了他们返回的新对象,或修改传递给它的原始对象。如果你的整个语言是不可变的,那么你知道你总是得到一个新的(复制&可能已修改)的结果,永远不必担心在将变量传递给函数之前意外修改变量。

你知道哪个会返回一个新的对象,哪个会改变下面例子中的原始对象吗?

Array.prototype.push()
String.prototype.slice()
Array.prototype.splice()
String.prototype.trim()

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

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

如果你看到任何一段代码被认为是“纯功能性的”;它是这样做的(不是真正的语言):

printUpToTen = map println [1..10]

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

printUpToTen = map println stdout [1..10]

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

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

想想以前的GameBoy Advance:为了在屏幕上显示某些内容,你必须修改内存(游戏邦注:有一些特定的地址会在一秒钟内被读取多次,从而决定在屏幕上显示什么内容)。您的计算模型(纯函数式编程)可能不需要状态来操作,您甚至可以使用命令来实现您的模型,状态操作模型(如汇编)抽象了状态操作,但在一天结束时,在代码的某个地方,您必须修改内存中的这些地址,以便设备实际显示任何内容。

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

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

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

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

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

你不。

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

约翰·巴克斯

(由我强调)

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

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

Haskell清洁这样的函数式语言允许你很容易地将这种观察付诸实践:大多数定义都是普通的ol'函数,就像你在数学教育中看到的那样。这就只剩下一小群“鱼龙混杂”的人了。然后处理所有恼人的外部状态,例如:

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

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


有时,如果您试图实现的算法使用私有、局部可变状态实现,则效果最好。在这种情况下,你可以使用Haskell扩展来完成没有,使整个程序“内部杂乱无章”。-详见John Launchbury和Simon Peyton Jones的国家在哈斯克尔