无状态编程的优势?


131

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

值得继续深入学习Haskell(或另一种纯功能语言)吗?函数式或无状态编程实际上比过程式生产率更高吗?以后我可能会继续使用Haskell或其他功能语言,还是应该仅出于理解目的学习它?

我更关心性能而不是生产力。所以我主要是在问我是否会比在过程/面向对象/什么方面的语言在功能语言上更有效率。

Answers:


167

简而言之,阅读函数式编程

无状态编程具有许多优点,其中尤其是显着的多线程和并发代码。坦率地说,可变状态是多线程代码的敌人。如果默认情况下值是不可变的,那么程序员无需担心一个线程会改变两个线程之间共享状态的值,因此它消除了与竞争条件相关的一类多线程错误。由于没有竞争条件,也没有理由使用锁,因此不变性也消除了另一类与死锁有关的错误。

这就是函数式编程如此重要的重要原因,并且可能是跳入函数式编程之路的最佳选择。与其他很大程度上依赖于设计模式的语言相比,还有很多其他好处,包括简化的调试(即,函数是纯净的,并且不会在应用程序的其他部分中改变状态),更简洁,更具表现力的代码,更少的样板代码。编译器可以更积极地优化您的代码。


5
我第二!我相信函数式编程将在将来更加广泛地使用,因为它适合并行编程。
Ray Hidayat,2009年

@Ray:我还将添加分布式编程!
安东·蒂克

除调试外,大多数情况都是正确的。通常,在haskell中比较困难,因为您没有真正的调用堆栈,而只有模式匹配堆栈。而且,很难预测代码的最终结果。
hasufell

3
同样,函数式编程并不是真正意义上的“无状态”。递归已经是隐式(本地)状态,这是我们在haskell中要做的主要事情。一旦您在惯用的haskell中实现了一些非平凡的算法(例如,计算几何学的东西)并对其进行调试很有趣,这一点就会变得很清楚。
hasufell

2
不喜欢将无状态等同于FP。许多FP程序都充满状态,它仅存在于闭包中而不是对象中。
mikemaccana

46

程序中无状态的片段越多,将各部分组合在一起而不会造成任何中断的方法就越多。无状态范式的能力本身并不在于无状态(或纯度),而在于它赋予您编写强大的,可重用的功能并将其组合的能力。

您可以在John Hughes的论文《功能编程为何如此重要(PDF)》中找到包含许多示例的优秀教程。

你会采空区更富有成效,特别是如果你选择一个功能性的语言,也有代数的数据类型和模式匹配(CAML,SML,哈斯克尔)。


mixins不会以与OOP类似的方式提供可重用的代码吗?不提倡OOP只是试图自己理解事物。
mikemaccana

20

其他许多答案都集中在函数式编程的性能(并行性)方面,我认为这很重要。但是,您确实特别询问过生产力,例如,在功能范例中您可以比在命令范例中更快地对同一事物进行编程。

我实际上(根据个人经验)发现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#以来,我意识到我发现以函数方式编写代码更容易,如果要过滤,则编写“ filter”,如果要映射,则编写“ map”,而不是实现每个细节。

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

因此,总的来说,即使我可以同时使用C#和F#进行编程,但我发现使用F#更容易,因为您可以在更高层次上进行思考。我会争辩说,因为较小的细节已从函数式编程中删除(至少在F#中已删除),所以我的工作效率更高。

编辑:我在其中的一条注释中看到您要求使用功能编程语言编写“状态”示例。F#可以强制性地编写,因此这是如何在F#中具有可变状态的直接示例:

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

1
我总体上同意您的文章,但|>与函数式编程本身无关。实际上,a |> b (p1, p2)只是的语法糖b (a, p1, p2)。将此与右联想结合起来,您就可以了。
2009年

2
没错,我应该承认,我在F#方面的积极经验可能与F#的关系远多于与函数编程的关系。但是,两者之间仍然有很强的相关性,即使类型推断和|>之类的东西本身并不是函数式编程,但我肯定会声称它们“与领域并存”。至少在一般情况下。
Ray Hidayat,2009年

|>只是另一个高阶中缀函数,在这种情况下为函数应用程序运算符。定义自己的高阶infix运算符绝对是功能编程的一部分(除非您是Schemer)。Haskell的$相同,只是管道中的信息从右到左流动。
诺曼·拉姆齐

15

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

现在,这些错误中有多少是归因于程序两个单独组件之间的“意外交互”?(几乎所有的线程错误有这样的形式:包括写共享数据,死锁的比赛,......另外,是很常见的发现,对全球状态的一些意想不到的效果库,或读/写注册表/环境等) 将假定至少有三分之一的“硬错误”属于此类。

现在,如果您切换到无状态/不可变/纯编程,那么所有这些错误都会消失。而是给您带来了一些新的挑战(例如,当您确实希望不同的模块与环境交互时),但是在像Haskell这样的语言中,这些交互被明确地化为类型系统,这意味着您可以仅查看类型有关与程序其余部分可能具有的交互类型的功能和原因。

这是来自“不变性” IMO的巨大胜利。在理想的世界中,我们都会设计出色的API,即使事情是易变的,效果也将是局部的且有据可查,并且“意外”交互将保持在最低限度。在现实世界中,有许多API以多种方式与全局状态交互,而这些API都是最有害的错误的源头。渴望无国籍状态是希望摆脱组件之间的意外/隐式/幕后互动。


6
有人曾经说过,覆盖一个可变值意味着您显式地在垃圾回收/释放先前的值。在某些情况下,程序的其他部分并未使用该值完成。当值无法突变时,此类错误也将消失。
shapr

8

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

但是效率不是唯一的问题。由于明确声明了影响纯函数的所有内容,因此它更易于测试和调试。而且,当使用一种功能语言进行编程时,人们习惯于使“脏”(使用I / O等)功能尽可能少。用这种方式分离出有状态的东西是设计程序的好方法,即使是在功能不太强大的语言中也是如此。

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


第一部分很有趣,我之前从未想过。谢谢!
2009年

假设您sin(PI/3)的代码中PI为常数,则编译器可以在编译时评估此函数并将结果嵌入生成的代码中。
Artelius

6

没有状态,自动并行化代码非常容易(由于CPU的内核越来越多,这一点非常重要)。


是的,我肯定已经调查过了。Erlang的并发模型特别吸引人。但是,在这一点上,我并没有真正关心并发性,而是关心生产力。在没有状态的情况下进行编程是否会带来生产率的提高?
Sasha Chedygov,2009年

2
@musicfreak,不,没有生产力奖励。但是请注意,如果您确实需要状态,现代FP语言仍然可以让您使用状态。
未知

真?您能否用功能语言给出状态的示例,以便我可以看到它是如何完成的?
2009年


4
@未知:我不同意。在没有状态的情况下进行编程可以减少由于不同组件的意外/意外交互而导致的错误的发生。它还鼓励更好的设计(更多的可重用性,机制和策略的分离以及诸如此类的东西)。它并不总是适合于当前的任务,但在某些情况下确实很有用。
Artelius

6

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

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

负载平衡器通常具有“粘性会话”的能力,其中负载平衡器以某种方式知道向用户发送请求的服务器。但是,这不是理想的选择,例如,这意味着每次您重新启动Web应用程序时,所有连接的用户都将丢失会话。

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.