无状态编程的优势?

我最近一直在学习函数式编程(特别是 Haskell,但是我也学习了 Lisp 和 Erlang 的教程)。虽然我发现这些概念非常有启发性,但我仍然看不到“无副作用”概念的实用性。它的实际优点是什么?我试图用功能性思维来思考,但是有些情况看起来过于复杂,没有能力以一种简单的方式拯救状态(我不认为 Haskell 的 monads 是“简单的”)。

继续深入学习 Haskell (或其他纯函数式语言)值得吗?函数式编程或无状态编程实际上比过程式编程更有生产力吗?我是否可能在以后继续使用 Haskell 或其他函数式语言,或者我只是为了理解才学习它?

我关心的不是绩效,而是工作效率。所以我主要问的是,我在函数式语言中的工作效率是否会高于过程式语言、面向对象语言或其他语言。

65906 次浏览

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

阅读 简单的函数式编程

无状态编程有许多优点,其中不仅仅是 戏剧性的多线程和并发代码。坦率地说,可变状态是多线程代码的敌人。如果值在默认情况下是不可变的,程序员就不需要担心一个线程会使两个线程之间的共享状态值发生变化,因此它消除了一整类与竞态条件相关的多线程 bug。因为没有竞态条件,所以也没有理由使用锁,所以不可变性也消除了另一类与死锁相关的 bug。

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

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

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

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

你的程序中越多的片段是无状态的,the more ways there are to put pieces together without having anything break。无状态范例的力量不在于无状态(或纯粹性) 本质上,而在于它赋予您编写强大的 可重复使用函数并将它们组合在一起的能力。

你可以在 John Hughes 的论文 为什么函数式编程很重要(PDF)中找到一个很好的教程,里面有很多例子。

如果你选择的函数式语言也包含代数数据类型和模式匹配(Caml、 SML、 Haskell) ,那么你的 大嘴巴会更有效率。

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

事实上,我发现(从个人经验来看)用 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 # 中做一个过滤器然后一个 map (每个元素的转换)会非常简单,但是你必须从更低的层次来思考。特别是,您必须编写循环本身,并且具有自己的显式 if 语句,以及诸如此类的东西。自从学习了 F # ,我发现用函数的方式编码更容易,如果你想要过滤,你写“ filter”,如果你想要映射,你写“ map”,而不是实现每个细节。

我还喜欢 | > 操作符,我认为它将 F # 与 ocaml 分开,可能还有其他函数式语言。它是管道运算符,它允许您将一个表达式的输出“管道”到另一个表达式的输入中。它让代码更符合我的思维方式。就像在上面的代码片段中说的,“获取因子序列,过滤它,然后映射它。”这是一个非常高水平的思考过程,在命令式编程语言中是不可能做到的,因为你忙于编写循环和 if 语句。每当我学习另一种语言时,这是我最怀念的事情。

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

Edit : 我在一个注释中看到您要求在函数式编程语言中使用“ state”的示例。F # 可以写成命令式的,所以这里有一个直接的例子说明如何在 F # 中使用可变状态:

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

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

现在,这些错误中有多少是由于程序的两个独立组件之间的“意外交互”造成的?(几乎所有的线程 bug 都有这种形式: 包括写入共享数据、死锁、 ... ... 此外,常常会发现库对全局状态有一些意想不到的影响,或者读/写注册表/环境,等等) 会假定至少三分之一的“硬 bug”属于这一类。

现在,如果您切换到无状态/不可变/纯编程,所有这些 bug 都会消失。相反,你会遇到一些新的挑战(例如,当你的 想要不同的模块与环境交互时) ,但是在像 Haskell 这样的语言中,这些交互被明确地具体化为类型系统,这意味着你可以只看一个函数的类型,并且推断它可以与程序的其余部分进行交互的类型。

这是“永恒”国际海事组织的重大胜利。在理想的情况下,我们都会设计出非常棒的 API,即使事情是可变的,效果也会是局部的,并且有良好的文档记录,而且“意想不到的”交互会被保持在最低限度。在现实世界中,有许多 API 以各种方式与全局状态进行交互,这些 API 是最有害的 bug 的来源。渴望无状态就是渴望摆脱组件之间无意识的/隐式的/幕后的交互。

不久前,我就这个主题写了一篇文章: 论纯洁的重要性

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

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

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

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

我的理解是 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 的情况下工作。

Haskel 和 Prolog 是可以作为无状态编程语言实现的语言的很好例子。但不幸的是,他们没有这么远。Prolog和 Haskel 目前都有强制实现。看到一些 SMT的,似乎更接近无骨编码。

这就是为什么您很难从这些编程语言中看到任何好处的原因。由于必要的实现,我们没有性能和稳定性方面的好处。因此,无状态语言基础结构的缺乏是您感觉没有任何无状态编程语言的主要原因,因为它的缺乏。

以下是纯粹无国籍的一些好处:

  • 任务描述是程序(紧凑代码)
  • 由于缺少依赖于状态的 bug (大多数 bug)而导致的稳定性
  • 可缓存的结果(一组输入总是导致相同的一组输出)
  • 可分发计算
  • 量子计算可重新计算基数
  • 多个重叠子句的瘦代码
  • 允许可微的编程优化
  • 始终如一地应用代码更改(添加逻辑不会破坏任何已写入的内容)
  • 优化组合数学(不需要强制枚举)

无状态编码是关注于数据之间的关系,然后通过推导将这些关系用于计算。基本上,这是编程抽象的下一个层次。它比任何命令式编程语言都更接近母语,因为它允许描述关系,而不是状态变化序列。