无状态编程的优点?

时间:2009-05-10 02:09:53

标签: functional-programming state immutability

我最近一直在学习函数式编程(特别是Haskell,但我也经历了关于Lisp和Erlang的教程)。虽然我发现这些概念非常具有启发性,但我仍然没有看到“无副作用”概念的实际方面。它有什么实际优势?我试图在功能思维中思考,但是有些情况看起来过于复杂而没有能够以简单的方式保存状态(我不认为Haskell的monad很容易')。

是否值得继续深入学习Haskell(或其他纯函数式语言)?功能性或无状态编程实际上比程序性更有效吗?我可能会在以后继续使用Haskell或其他功能语言,还是应该仅仅为了理解而学习它?

我关心的是绩效而不是生产力。所以我主要问的是我是否会在函数式语言中比在程序/面向对象/其他方面更高效。

9 个答案:

答案 0 :(得分:158)

阅读Functional Programming in a Nutshell

无状态编程有很多优点,其中最重要的是显着多线程和并发代码。说白了,可变状态是多线程代码的敌人。如果值默认是不可变的,程序员不需要担心一个线程在两个线程之间改变共享状态的值,因此它消除了与竞争条件相关的整类多线程错误。由于没有竞争条件,因此也没有理由使用锁,因此不变性也消除了与死锁相关的另一类错误。

这就是功能编程很重要的一个重要原因,也许是跳过功能编程系列的最佳选择。还有许多其他好处,包括简化的调试(即函数是纯粹的,不会在应用程序的其他部分中改变状态),更简洁和富有表现力的代码,与严重依赖于设计模式的语言相比更少的样板代码,以及编译器可以更积极地优化您的代码。

答案 1 :(得分:42)

您的程序中的更多部分是无状态的,将部分放在一起而没有任何中断的方式越多。无状态范式的力量不在于无状态(或纯度)本身,而在于它能够让你编写强大的,可重用的函数并将它们组合起来。

你可以在John Hughes的论文Why Functional Programming Matters(PDF)中找到一个包含大量例子的好教程。

您将 gobs 更高效,特别是如果您选择的函数语言也具有代数数据类型和模式匹配(Caml,SML,Haskell)。

答案 2 :(得分:19)

许多其他答案都集中在函数式编程的性能(并行性)方面,我认为这非常重要。但是,你确实特别询问了生产力,因为你可以在函数范式中比在命令式范例中更快地编写相同的东西。

我实际上(根据个人经验)发现F#中的编程与我认为更好的方式匹配,因此更容易。我认为这是最大的区别。我已经用F#和C#进行了编程,并且F#中的“语言对抗”要少得多,我很喜欢。您不必考虑F#中的细节。以下是我发现的非常喜欢的一些例子。

例如,即使F#是静态类型的(所有类型都在编译时解析),类型推断也会确定您拥有的类型,因此您不必说出来。如果它无法弄明白,它会自动使你的函数/类/通用。所以你永远不必写任何通用的东西,它都是自动的。我发现这意味着我花更多时间思考问题,而不是如何实现它。事实上,每当我回到C#时,我发现我真的很想念这种类型的推理,你永远不会意识到它是多么分散注意力,直到你不再需要它为止。

同样在F#中,您可以调用函数,而不是编写循环。这是一个微妙的变化,但很重要,因为你不必再考虑循环结构了。例如,这里有一段代码可以通过并匹配某些内容(我不记得是什么,它来自项目Euler难题):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

我意识到在C#中使用过滤器然后进行地图(这是每个元素的转换)将非常简单,但您必须在较低级别进行思考。特别是,您必须编写循环本身,并拥有自己的显式if语句和这些类型的东西。自学习F#以来,我意识到我发现以功能方式编写代码更容易,如果你想过滤,你可以编写“过滤器”,如果要映射,你可以编写“map”而不是实现每个细节。

我也喜欢|>运算符,我认为将F#与ocaml分开,可能还有其他函数式语言。它是管道运算符,它允许您将一个表达式的输出“管道”到另一个表达式的输入中。它使代码遵循我的想法。就像在上面的代码片段中那样,“采用因子序列,过滤它,然后映射它”。这是一个非常高水平的思考,你不会使用命令式编程语言,因为你忙于编写循环和if语句。每当我使用另一种语言时,这是我最想念的一件事。

一般来说,即使我可以在C#和F#中编程,我发现使用F#更容易,因为你可以在更高的层次上思考。我认为,因为从功能编程中删除了较小的细节(至少在F#中),我的工作效率更高。

编辑:我在其中一条评论中看到您在函数式编程语言中要求提供“状态”的示例。 F#可以强制写入,所以这里有一个直接的例子说明你如何在F#中拥有可变状态:

let mutable x = 5
for i in 1..10 do
    x <- x + i

答案 3 :(得分:14)

考虑你花了很长时间调试的所有困难错误。

现在,有多少错误是由于程序的两个独立组件之间的“意外交互”造成的? (几乎所有的线程错误都有这种形式:涉及编写共享数据,死锁等的竞争......此外,通常会发现对全局状态有一些意外影响的库,或读/写注册表/环境等等)< em>我会认为至少有三分之一的'难题'属于这一类。

现在,如果你切换到无状态/不可变/纯编程,所有这些错误就会消失。您会遇到一些新的挑战(例如,当您 希望不同的模块与环境交互时),但在像Haskell这样的语言中,这些交互会被明确地引入类型系统,这意味着你可以查看一个函数的类型以及它与程序其余部分可以进行的交互类型的原因。

这是'不变性'IMO的重大胜利。在理想的世界中,我们都设计了极好的API,即使事情是可变的,效果也会是局部的,并且记录良好,并且“意外”的交互将保持在最低限度。在现实世界中,有许多API以无数种方式与全球状态相互作用,这些是最有害的错误的来源。渴望无国籍状态有望摆脱组件之间的无意/隐含/幕后交互。

答案 4 :(得分:7)

无状态函数的一个优点是它们允许预先计算或缓存函数的返回值。甚至一些C编译器允许您明确地将函数标记为无状态以提高其可优化性。正如许多其他人所指出的那样,无状态函数更容易并行化。

但效率不是唯一的问题。纯函数更容易测试和调试,因为任何影响它的东西都是明确说明的。当用函数式语言编程时,人们习惯于尽可能少地使用“脏”(使用I / O等)函数。以这种方式分离有状态的东西是设计程序的好方法,即使在不那么功能的语言中也是如此。

功能语言可能需要一段时间来“获取”,并且很难向没有经历过该过程的人解释。但是大多数持续时间足够长的人终于意识到大惊小怪是值得的,即使他们最终没有使用功能语言。

答案 5 :(得分:5)

没有状态,很容易自动并行化你的代码(因为CPU是由越来越多的内核组成的,这非常重要)。

答案 6 :(得分:4)

我在一段时间内写了一篇关于这个主题的帖子:On The Importance of Purity

答案 7 :(得分:4)

当您开始拥有更高的流量时,无状态Web应用程序是必不可少的。

例如,出于安全原因,您可能不希望在客户端存储大量用户数据。在这种情况下,您需要将其存储在服务器端。您可以使用Web应用程序默认会话,但如果您有多个应用程序实例,则需要确保每个用户始终定向到同一个实例。

负载均衡器通常具有“粘性会话”的能力,其中负载均衡器可以知道哪个服务器向用户发送请求。这并不理想,例如,这意味着每次重新启动Web应用程序时,所有连接的用户都将失去会话。

更好的方法是将Web服务器后面的会话存储在某种数据存储中,现在有很多很好的nosql产品可用于此(redis,mongo,elasticsearch,memcached)。这样,Web服务器是无状态的,但您仍然具有状态服务器端,并且可以通过选择正确的数据存储设置来管理此状态的可用性。这些数据存储通常具有很强的冗余性,因此几乎总是可以对Web应用程序甚至数据存储进行更改,而不会影响用户。

答案 8 :(得分:2)

我的理解是 FP 对测试也有巨大的影响。没有可变状态通常会迫使您向函数提供比类更多的数据。有一些权衡,但想想测试一个“incrementNumberByN”而不是“Counter”类的函数是多么容易。

对象

describe("counter", () => {
    it("should increment the count by one when 'increment' invoked without 
    argument", () => {
       const counter = new Counter(0)
       counter.increment()
       expect(counter.count).toBe(1)
    })
   it("should increment the count by n when 'increment' invoked with 
    argument", () => {
       const counter = new Counter(0)
       counter.increment(2)
       expect(counter.count).toBe(2)
    })
})

功能

 describe("incrementNumberBy(startingNumber, increment)", () => {

   it("should increment by 1 if n not supplied"){
      expect(incrementNumberBy(0)).toBe(1)
   }

   it("should increment by 1 if n = 1 supplied"){
      expect(countBy(0, 1)).toBe(1)
   }

 })

由于函数没有状态并且输入的数据更加明确,因此当您试图找出测试可能失败的原因时,需要关注的事情就更少了。关于我们必须做的柜台测试

       const counter = new Counter(0)
       counter.increment()
       expect(counter.count).toBe(1)

前两行都对 counter.count 的值有贡献。在像这样的简单示例中,1 对 2 行可能有问题的代码没什么大不了的,但是当您处理更复杂的对象时,您可能也会为测试增加大量复杂性。

相反,当您使用函数式语言编写项目时,它会促使您使花哨的算法依赖于流入和流出特定函数的数据,而不是依赖于系统的状态。

另一种看待它的方式是说明在每种范式中测试系统的思维方式。

对于函数式编程:确保函数 A 对给定输入有效,确保函数 B 对给定输入有效,确保 C 对给定输入有效。

对于 OOP:在对对象的状态执行 Y 和 Z 之后,确保对象 A 的方法在给定 X 输入参数的情况下工作。在对对象的状态执行 W 和 Y 之后,确保对象 B 的方法在给定 X 的输入参数的情况下工作。