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

最近我一直在阅读很多关于函数式编程的东西,而且我可以理解它的大部分内容,但是我无法用脑袋包装的东西是无状态编码。 在我看来,通过消除可变状态简化编程就像是通过移除仪表板来“简化”一辆汽车:成品可能更简单,但运气好,可以与最终用户交互。

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

每当我find讨论这个问题的东西时,就会写出真正的技术function – 这就是假设我没有的大量FP背景。 有没有人知道一个方法来解释这个对命令编码有良好的,扎实的理解的人,但是谁在function方面是一个完整的n00b?

编辑:到目前为止答复的一堆似乎试图说服我的不变值的优点。 我得到那部分。 这很有道理。 我不明白的是,如何跟踪必须改变的值,并不断变化,而不会有可变的variables。

或者如果你玩电子游戏,那么就会有大量的状态variables,从所有angular色的位置开始,这些angular色会不断地移动。 你怎么可能做任何有用的事情,而不跟踪价值的变化?

如果你有兴趣,下面是一系列描述Erlang游戏编程的文章。

你可能不会喜欢这个答案,但是直到你使用它才会得到function程序。 我可以发布代码示例,并说“这里,你没有看到 ” – 但是如果你不明白的语法和基本原则,那么你的眼睛只是釉下来。 从你的angular度来看,我看起来好像是在做一个命令式语言一样的事情,但是只是设置了各种界限来有目的地使编程更加困难。 我的观点,你只是经历了Blub悖论 。

起初我一直很怀疑,但是几年前我跳上function性编程列车,并且爱上了它。 函数式编程的技巧是能够识别模式,特定的variables赋值,并将命令状态移动到堆栈。 例如for循环就成为recursion:

// 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方法将通过集合枚举并为每个项目调用匿名函数。 非常便利 :)

我知道,印刷数字并不令人印象深刻。 但是,我们可以在游戏中使用相同的方法:在堆栈中保存所有状态,并使用recursion调用中的更改创build一个新对象。 通过这种方式,每一帧都是游戏的无状态快照,每一帧都可以创build一个全新的对象,并随着无状态对象需要更新所需的更改。 这个伪代码可能是:

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

命令式和function式版本是相同的,但是function版本明显不使用可变状态。 function代码保持所有状态都保存在堆栈上 – 这种方法的好处是,如果出现问题,debugging很容易,只需要一个堆栈跟踪。

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

几乎所有我能想到的用户应用程序都将状态作为核心概念。

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

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

上面的代码构造了两个不可变列表,将它们附加在一起以创build一个新列表,并追加结果。 在应用程序的任何地方都没有使用可变状态。 它看起来有点笨重,但那只是因为C#是一个冗长的语言。 以下是F#中的等效程序:

 type 'a stack = | Cons of 'a * 'a stack | Nil let rec append xy = 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 xy iter (fun a -> printfn "%i" a) z 

没有必要创build和操作列表mutable。 几乎所有的数据结构都可以很容易地转换成function等价物。 我在这里写了一个页面,它提供了堆栈,队列,左边的堆,红黑树,懒惰列表的不可变实现。 没有一个代码片段包含任何可变状态。 为了“改变”一棵树,我创build了一个全新的节点,我想要的 – 这是非常有效的,因为我不需要复制树中的每个节点,我可以在我的新的树。

使用更重要的例子,我也写了这个完全无状态的SQLparsing器 (或者至less我的代码是无状态的,我不知道底层的lexing库是否是无状态的)。

无状态编程和有状态编程一样具有performance力和强大function,只需要一些练习来训练自己开始无状态思考。 当然,“尽可能无状态编程,必要时有状态编程”似乎是大多数不纯function语言的座右铭。 当function性方法不够干净或有效时,回避可变性是没有害处的。

简短的回答:你不能。

那么,不变性有什么大不了的呢?

如果你熟悉命令式语言,那么你知道“全局是坏的”。 为什么? 因为他们在代码中引入(或有可能引入)一些非常难以解决的依赖关系。 依赖关系不好; 你希望你的代码是模块化的 。 程序的一部分不会影响其他部分尽可能less。 而FP带给你模块化的圣杯:完全没有任何副作用。 你只需要你的f(x)= y。 把x放进去,让出来。 没有更改x或其他任何东西。 FP让你停止思考状态,并开始思考价值。 所有你的函数只是接收值并产生新的值。

这有几个好处。

首先,没有任何副作用意味着更简单的程序,更容易推理。 不用担心,引入新的程序部分将会干扰和破坏现有的工作部分。

其次,这使程序平行化(高效的并行化是另一回事)。

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

 double x = 2 * x 

现在你input3的值,你得到6的值。 每次。 但是你也可以做到这一点,对不对? 是的。 但问题是,在必要的时候,你可以做更多 。 我可以:

 int y = 2; int double(x){ return x * y; } 

但我也可以做

 int y = 2; int double(x){ return x * (y++); } 

命令式的编译器不知道我是否会产生副作用,这使得优化变得更加困难(也就是说double 2不必每次都是4)。 function的人知道我不会 – 因此,它可以优化每次看到“双2”。

现在,就计算机内存而言,即使每次创build新的值对于复杂types的值来说似乎都是非常浪费的,但并不一定如此。 因为如果你有f(x)= y,并且x和y的值“大部分是相同的”(例如只有几片叶子不同的树),那么x和y可以共享部分内存 – 因为它们都不会变异。

所以如果这个不可改变的事情是如此之大,为什么我回答说,你不能做任何有用的,没有可变状态。 那么,没有可变性,你的整个程序将是一个巨大的f(x)= y函数。 而程序的所有部分都是一样的:只是function,而function则是“纯粹”的。 正如我所说,这意味着每次 f(x)= y。 所以例如readFile(“myFile.txt”)每次都需要返回相同的string值。 不太有用。

因此,每个FP提供了一些变异状态的手段。 “纯粹的”function语言(如Haskell)使用monad等有些可怕的概念来做到这一点,而“不纯”的(如ML)则直接允许这样做。

当然,函数式语言还有许多其他的function,使编程更有效率,比如一stream的function等等。

请注意,说function性编程没有“状态”是有点误导,可能是混乱的原因。 它绝对没有“可变状态”,但它仍然可以有被操纵的值; 他们只是不能就地改变(例如,你必须从旧值创build新的值)。

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

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

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

这不是一个build议的“解决scheme”,但最简单的方法来看到这将始终工作,你可以存储这些不可变的值到一个地图(字典/哈希表)像结构,键入一个“variables名”。

很显然,在实际的解决scheme中,你会使用更为理智的方法,但是这确实表明最坏的情况下,如果没有其他的工作,你可以使用这样一个地图来模拟可变状态。

下面是你如何编写没有可变状态的代码 :不是把变化的状态变成可变的variables,而是把它放到函数的参数中。 而不是写循环,你写recursion函数。 所以例如这个命令性的代码:

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

成为这个function代码(Scheme-like语法):

 (define (f-functional y) (letrec ( (f-helper (lambda (xy) (if (pxy) (f-helper (gxy) y) (hxy))))) (f-helper ey))) 

或Haskellish代码

 f_fun y = h x_final y where x_initial = e x_final = loop x_initial loop x = if pxy then loop (gxy) else x 

至于为什么function程序员喜欢这样做(你没有问),你的程序越是无状态, 就越有可能把件拼在一起而没有任何中断 。 无国籍范式的力量本身不在于无国籍(或纯洁),而在于它赋予你强大的, 可重复使用的function并将它们结合起来的能力。

你可以在约翰·休斯的论文“ Why Why Programming Matters”中find一个很好的教程。

这是做同样的事情的不同方式。

考虑一个简单的例子,例如添加数字3,5和10.想象一下,考虑通过首先将3的值加5,然后将10加到“3”,然后输出当前值“ 3“(18)。 这看起来非常荒谬,但实质上是基于状态的命令式编程经常完成的方式。 事实上,你可以有许多不同的值为3的“3”,但是却是不同的。 所有这些看起来都很奇怪,因为我们已经根深蒂固地认为数字是不可改变的。

现在想想当你把这些值设为不变的时候,加上3,5和10。 你添加3和5来产生另一个值,8,然后你加上10来产生另一个值18。

这些都是做同样事情的等价方法。 所有必要的信息都以两种方法存在,但forms不同。 其中一种信息是作为状态和状态变化的规则而存在的。 另一方面,信息存在于不可变数据和function定义中。

我觉得有一点误会。 纯function程序有状态。 不同之处在于该状态是如何build模的。 在纯粹的函数式编程中,状态由需要一些状态并返回下一个状态的函数来操纵。 然后通过状态传递一系列纯函数来实现对状态的sorting。

即使是全局可变状态也可以用这种方式build模。 例如,在Haskell中,程序是从世界到世界的function。 也就是说,你通过整个宇宙 ,程序返回一个新的宇宙。 但实际上,你只需要传入程序实际感兴趣的宇宙部分。 程序实际上会返回一系列操作 ,作为程序运行的操作环境的指令。

你想看看这个命令式编程的解释。 好的,让我们来看一些function语言中非常简单的命令式编程。

考虑这个代码:

 int x = 1; int y = x + 1; x = x + y; return x; 

漂亮的沼泽标准命令代码。 没有做任何有趣的事情,但是没关系。 我想你会同意这里有涉及的国家。 xvariables的值随时间而变化。 现在,我们通过发明一个新的语法来稍微改变符号:

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

所以你看,状态是由一系列纯expression式来模拟的,这些expression式绑定了下面expression式的自由variables。

你会发现这个模式可以模拟任何一种状态,甚至IO。

函数式编程避免了状态并强调了function。 从来没有任何国家没有这样的事情,虽然国家可能实际上是不可改变的东西,或烘烤到你正在工作的架构。 考虑一个静态的web服务器,它只是将文件从文件系统中加载到实现Rubik的多维数据集的程序之间。 前者将按照devise用于将请求转化为文件path请求的function实现为来自该文件内容的响应。 实际上,除了一点configuration之外,几乎不需要任何状态(文件系统的状态实际上超出了程序的范围,程序的工作方式与文件的状态无关)。 在后者中,您需要对多维数据集和您的程序实现进行build模,以了解该多维数据集上的操作如何改变其状态。

我迟到了讨论,但是我想为那些正在努力进行函数式编程的人添加几点。

  1. 函数式语言保持与命令式语言完全相同的状态更新,但是通过将更新的状态传递给后续的函数调用来完成 。 这是一个非常简单的例子,沿着一个数字行。 你的状态是你现在的位置。

首先是必要的方式(伪代码)

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

现在的function方式(伪代码)。 我非常依赖三元运算符,因为我希望来自命令式背景的人能够实际读取这些代码。 所以如果你不使用三元运算符(我总是避免它在我的命令的日子),这是它的工作原理。

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

您可以通过将新的三元expression式代替假expression式来链接三元expression式

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

所以考虑到这一点,这里是function版本。

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

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

  1. 经验教训是,函数式语言通过调用具有不同参数的函数来“变异”状态。 显然这并不会使任何variables发生变化,但是这就是如何得到类似的效果。 这意味着如果你想做function性编程,你必须习惯于recursion地思考。

  2. 学习recursion思考并不困难,但它确实需要练习和工具包。 在那个“学习Java”的书中,他们用recursion来计算阶乘的那一小节并没有削减它。 你需要一套技巧,比如使迭代过程脱离recursion(这就是为什么尾recursion对于函数式语言来说是必不可less的),延续,不variables等。如果不了解访问修饰符,接口等,就不会进行OO编程。用于function性编程。

我的build议是做小Schemer(注意我说“做”而不是“读”),然后在SICP中做所有的练习。 当你完成了,你将有一个不同的大脑,当你开始。

除了别人给出的很好的答案之外,还要考虑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()); 

你已经默默地改变了地图上的一个键! 使用不可变的对象,比如在函数式编程中,是非常干净的。 有什么副作用发生是比较容易的 – 没有! 这意味着程序员更容易,优化器也更容易。

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

考虑一个types为s -> (a, s)的函数。 从Haskell语法转换来说,它意味着一个函数,它接受一个“ s ”types的参数并返回一对“ a ”和“ s ”types的值。 如果s是我们状态的types,那么这个函数接受一个状态并返回一个新的状态,可能还有一个值(你总是可以返回“unit”aka () ,这在C / C ++中相当于“ void ”作为“ a ”型)。 如果你用这样的types链接几个函数的调用(从一个函数返回状态并传递给下一个函数),你有“可变”状态(实际上你在每个函数创build一个新的状态,并放弃旧的)。

如果将可变状态想象为执行程序的“空间”,然后再考虑时间维度,可能会更容易理解。 在时刻t1,“空间”处于某种状态(例如,某个存储单元的值为5)。 在稍后的时刻t2,它处于不同的状态(例如,存储位置现在具有值10)。 这些时间中的每一个“切片”都是一个状态,而且是不变的(你不能再回头去改变它们)。 所以,从这个angular度来看,你从一个时间箭头(你的可变状态)到一个时空切片(几个不可变的状态),并且你的程序只是把每个切片看作一个值并计算每个其中的一个作为一个function适用于前一个。

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

将整个程序状态明确地表示为一个值,这个值只能在下一个时刻被丢弃(在创build新的时刻之后)。 对于一些algorithm,这可能是很自然的,但是当它不是时,还有另一个窍门。 而不是一个真实的状态,你可以使用一个假的状态,它只不过是一个标记(我们称之为假状态的State#的types)。 这个假状态从语言的angular度来看是存在的,并且像任何其他值一样传递,但是编译器在生成机器码时完全省略了它。 它只是用来标记执行的顺序。

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

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

从这些类似Haskell的声明中, readRef接收类似于指针或句柄的types为“ a ”的值和假状态的东西,并且返回由第一个参数指向的types“ a ”的值和新的假的状态。 writeRef是相似的,但是改变指向的值。

如果您调用readRef ,然后将writeRef返回的假状态(可能是其他调用中间不相关的函数;这些状态值创build函数调用的“链”)传递给它,它将返回写入的值。 你可以用相同的指针/句柄再次调用writeRef ,它将写入同一个内存位置 – 但是从概念上说,它返回一个新的(假的)状态,(假的)状态仍然是不可变的(一个新的状态是“创build“)。 如果有一个实际的状态variables需要计算,编译器会按照它们必须调用的顺序调用函数,但是唯一的状态是实际硬件的完全(可变)状态。

(那些知道Haskell的人会注意到我简化了很多东西,忽略了一些重要的细节,对于那些想了解更多细节的人来说,看一下来自mtl Control.Monad.State ,以及ST sIO (也就是ST RealWorld )monads。)

你可能想知道为什么要这么迂回(而不是简单地在语言中有可变的状态)。 真正的好处是你已经确定了程序的状态。 以前是隐含的(你的程序状态是全局的,允许远程行动等)现在是明确的。 不接收和返回状态的函数不能修改或受其影响; 他们是“纯”的。 更妙的是,你可以有独立的状态线程,并且有一些types的魔术,它们可以被用来在一个纯粹的命令式计算中embedded,而不会使它不纯(Haskell中的ST monad是通常用于这个技巧的那个;我上面提到的State# s实际上是GHC的State# s ,通过实施STIO monads使用)。

使用一些创造性和模式匹配,创build了无状态游戏:

  • CSSPlay:迷宫游戏
  • CSSPlay:迷宫游戏2
  • CSSPlay:Tic-Tac-Toe
  • 纯粹的CSS Tic-Tac-Toe
  • CSSPlay:庞
  • CSSPlay:Ping-Pong
  • CSSPlay:警察和强盗
  • CSSPlay:重击 – 鼠
  • CSS3庞:疯狂的事情与CSS

以及滚动演示:

  • CSSPlay:随机英雄
  • animation模拟SVG时钟
  • animationSVG摆
  • animationSVG赛车手
  • CSS3:制造雪

和可视化:

  • XSLT Mandlebrot

这就是FORTRAN在没有COMMON块的情况下工作的方式:你可以编写具有传入的值和局部variables的方法。 而已。

面向对象程序devise给我们带来了状态和行为,但是在1994年我第一次遇到它时,这是一个新的想法。

Geez,当我是一名机械工程师时,我是个function程序员,我不知道!

对于高度互动的应用程序,例如游戏, Functional Reactive Programming是您的朋友:如果您可以将游戏世界的属性expression为时变的值 (和/或事件stream),那么您已经准备好了! 这些公式有时甚至更自然,更有意义 – 比变化状态更能揭示出来,比如对于一个移动的球,你可以直接使用众所周知的法则x = v * t 。 更好的是,这样写的游戏规则比面向对象的抽象更好。 例如,在这种情况下,球的速度也可以是随时间变化的值,其取决于由球的碰撞组成的事件stream。 有关更具体的devise考虑,请参阅在Elm中制作游戏 。

请记住:函数式语言是图灵完整的。 因此,你可以用一种function语言来完成任何有用的任务。 尽pipe如此,我认为还有一点要说的是混合方法。 像F#和Clojure这样的语言(我相信其他人)鼓励无国籍的devise,但在必要时允许可变性。

You can't have a pure functional language that is useful. There will always be a level of mutability that you have to deal with, IO is one example.

Think of functional languages as just another tool that you use. Its good for certain things, but not others. The game example you gave might not be the best way to use a functional language, at least the screen will have a mutable state that you can't do anything about with FP. The way you think of problem and the type of problems you solve with FP will be different from ones you are used to with imperative programming.

By using lots of recursion.

Tic Tac Toe in F# (A functional language.)

This is very simple. You can use as many variables as you want in functional programming…but only if they're local variables (contained inside functions). So just wrap your code in functions, pass values back and forth among those functions (as passed parameters and returned values)…and that's all there is to it!

这是一个例子:

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