为什么函数式编程应该成为软件开发的未来

图片

它很难学,但是你的代码会产生更少的意外

你可能认为软件产品生命周期中最长最昂贵的阶段是系统的初始开发阶段,因为所有那些伟大的功能都是在最初的想象中创建的。事实上,最困难的部分是之后的维护阶段。这是程序员为他们在开发过程中走捷径付出代价的时候。

那么他们为什么要走捷径呢?也许他们没有意识到他们在走捷径,只有当他们的代码被部署并被大量用户使用时,其隐藏的缺陷才会暴露出来,也许开发人员很着急,上市时间的压力几乎可以保证他们的软件会包含更多的错误。

大多数公司在维护代码方面的困难导致了第二个问题:脆弱性。每个添加到代码中的新功能都会增加其复杂性,从而增加某些东西崩溃的几率。软件变得如此复杂,以至于开发人员为了避免破坏某些东西而避免进行非绝对必要的更改,这很常见。在许多公司,整个开发团队的雇佣目的不是开发任何新的东西,而是保持现有系统的运行。你可能会说,他们运行的是软件版本的红方王后赛跑,他们尽可能快地跑,只是为了呆在同一个地方。

这是一个令人遗憾的情况。然而,当前软件行业的轨迹是朝着日益复杂、产品开发时间更长和生产系统更脆弱的方向发展。为了解决这些问题,公司通常只是在问题上投入更多的人:更多的开发人员、更多的测试人员和更多的技术人员,当系统出现故障时进行干预。

肯定有更好的方法,我属于一个正在成长的开发者群体,他们认为函数式编程可以解决这个问题。在这里,我将描述什么是函数式编程,为什么使用它会有帮助,以及为什么我对它如此热衷。


在函数式编程中,少即是多
 

理解函数式编程的基本原理的一个好方法是考虑半个多世纪前发生的事情。在20世纪60年代末,出现了一种编程范式,旨在提高代码质量,同时减少开发时间。它被称为结构化编程。

各种语言涌现出来以促进结构化编程,一些现有语言也被修改以更好地支持结构化编程。这些结构化编程语言最显著的特性之一根本不是什么特性:它缺少了存在很长时间的东西——GOTO语句。

GOTO语句用于重定向程序执行,它不是依次执行下一条语句,而是在满足某个条件时,将程序流重定向到GOTO行中指定的其他语句。

取消GOTO是基于程序员们从使用它中了解到的——它使程序很难理解,带有GOTO的程序通常被称为意大利面代码,因为执行的指令序列可能像一碗意大利面中的单个线一样难以跟踪。

图片

这些开发人员无法理解他们的代码是如何工作的,或者为什么它有时不能工作,这是一个复杂性问题。那个时代的软件专家认为,那些GOTO语句 正在创造不必要的复杂性,GOTO必须,嗯,去掉。

在当时,这是一个激进的想法,许多程序员拒绝失去他们已经依赖的语句。争论持续了十多年,但最终,GOTO 消失了,今天没有人会争论它的回归。这是因为从高级编程语言中消除它极大地降低了复杂性,提高了所生产的软件的可靠性。它通过限制程序员可以做的事情来做到这一点,这最终使他们更容易推断他们正在编写的代码。

尽管软件行业已经从现代高级语言中消除了GOTO,但软件的复杂性和脆弱性仍在继续增长。寻找如何修改这些编程语言以避免一些常见的陷阱,软件设计师可以从硬件方面的同行那里找到灵感。


用空引用解决问题
 

在设计计算机硬件时,你不能让一个电阻器被多个设备共享,比如键盘和显示器的电路。但程序员在他们的软件中一直都在做这种共享。这被称为共享全局状态:变量不属于任何一个进程,但可以被任何数量的进程更改,甚至是同时更改。

现在,想象一下,每次你打开微波炉,洗碗机的设置就会从正常循环切换到锅碗瓢盆。当然,这在现实世界中是不会发生的,但在软件中,这种事情时常发生。程序员编写调用函数的代码,希望它执行单个任务。但许多函数都有改变共享全局状态的副作用, 导致意想不到的结果。

在硬件领域,这种情况不会发生,因为物理定律限制了可能性。当然,硬件工程师可能会把事情搞砸,但这不像你在软件领域会遇到的情况,软件领域有太多事情是可能发生的,无论是好是坏。

另一个潜伏在软件泥沼中的复杂怪物被称为空引用,这意味着对内存中某个位置的引用指向的是什么都不是。如果你试图使用这个引用,就会发生错误。因此,程序员必须在试图读取或更改它所引用的内容之前检查它是否为空。

如今几乎每种流行语言都有这个缺陷。计算机科学家先驱Tony Hoare早在1965年就将空引用引入了ALGOL语言,后来又被纳入了许多其他语言中。Hoare解释说他这么做“只是因为它很容易实现”,但今天他认为这是一个“价值数十亿美元的错误”。这是因为当程序员期望的有效引用实际上是一个空引用时,它会导致无数错误。

软件开发人员需要严格遵守纪律来避免这种陷阱,但有时他们并没有采取足够的预防措施。结构化编程的架构师知道对于GOTO语句来说这是事实,并且没有给开发人员留下任何逃生出口。为了保证无GOTO代码所承诺的清晰度改进,他们知道他们必须从结构化编程语言中完全消除它。

历史证明,移除危险的特性可以极大地提高代码质量。今天,我们有大量危险的实践,这些实践会损害软件的健壮性和可维护性。几乎所有现代编程语言都有某种形式的空引用、共享全局状态和具有副作用的函数——这些东西比GOTO要糟糕得多。

如何消除这些缺陷?答案其实已经存在了几十年:纯函数式编程语言。

图片

在排名前十的函数式编程语言中,Haskell 是目前最流行的,这可以从使用这些语言的 GitHub 仓库的数量来判断。

第一个流行起来的纯函数语言Haskell诞生于1990年。所以到20世纪90年代中期,软件开发界真正有了解决至今仍面临的棘手问题的方案。遗憾的是,当时的硬件通常不够强大,无法利用这种解决方案。但今天的处理器可以轻松满足Haskell和其他纯函数语言的需求。

事实上,基于纯函数的软件特别适合现代多核CPU。这是因为纯函数只对输入参数进行操作,使得不同函数之间不可能有任何交互。这允许编译器进行优化,以生成高效且容易在多核上运行的代码。

正如名字所示,使用纯函数式编程,开发人员只能编写纯函数,根据定义,纯函数不能有副作用,有了这个限制,你就可以提高稳定性,为编译器优化打开大门,并最终得到更容易理解的代码。

但是如果一个函数需要知道或者需要操纵系统的状态呢?在这种情况下,状态通过一个称为复合函数的长链传递-函数将它们的输出传递给链中下一个函数的输入。通过将状态从函数传递到函数,每个函数都可以访问它,并且没有机会让另一个并发编程线程修改该状态-在太多的程序中发现的另一个常见的和昂贵的脆弱性。

避免空引用意外

Javascript和Purescript的比较显示了后者如何帮助程序员避免错误。

图片

函数式编程也有一个解决 Hoare 的“十亿美元错误”空引用的办法。它通过禁止空引用来解决这个问题。相反,它有一个通常称为 Maybe(在某些语言中称为 Option)的结构。Maybe 可以是 Nothing 或只是某个值。使用 Maybe 迫使开发人员总是考虑两种情况。他们在这个问题上没有选择。他们必须在每次遇到 Maybe 时处理 Nothing 情况。这样做可以消除空引用可能产生的许多 bug。

函数式编程也要求数据不可变,这意味着一旦你将变量设置为某个值,它就永远是那个值。变量更像数学中的变量。例如,要计算公式 y = x 2  2x - 11,你为x选择一个值,在计算y的过程中,x不会有不同的值。因此,在计算x 2  时使用的x值与计算2x时使用的x值相同。在大多数编程语言中,没有这样的限制。你可以用一个值计算x 2 ,然后在计算2x之前改变x的值。通过禁止开发人员改变(突变)值,他们可以使用他们在中学代数课上所做的相同推理。

与大多数语言不同,函数式编程语言深深植根于数学,正是这种在严格的数学领域中的血统赋予了函数式语言最大的优势。

为什么呢?因为人们已经研究数学几千年了,它非常可靠。大多数编程范式,比如面向对象编程,最多有几十年的历史,相比之下它们是粗糙和不成熟的。

让我来分享一个例子,说明编程与数学相比是多么的草率。当新程序员第一次遇到语句 x = x + 1 时,我们通常会教他们忘记在数学课上学过的东西。在数学中,这个方程没有解。但在当今大多数编程语言中,x = x + 1 不是一个方程。它是一个命令计算机获取 x 的值,向它加 1,然后把它放回一个名为 x 的变量中。

在函数式编程中,没有语句,只有表达式。我们在中学学到的数学思维现在可以用函数式语言来编写代码。

由于函数式语言的纯粹性,你可以使用代数替换来推理代码,以帮助降低代码的复杂性,就像你在代数课上降低方程的复杂性一样。在非函数式语言(命令式语言)中,没有等效的机制来推理代码是如何工作的。


函数式编程的学习曲线陡峭
 

纯函数式编程通过从语言中移除危险的特性,解决了我们行业中许多最大的问题,使开发人员更难搬起石头砸自己的脚。起初,这些限制可能看起来很严重,我相信20世纪60年代的开发人员对于移除GOTO的感受。但事实是,在这些语言中工作既解放又赋能,以至于几乎所有当今最流行的语言都纳入了函数式特性,尽管它们从根本上仍然是命令式语言。

这种混合方法的最大问题是,它仍然允许开发人员忽略语言的功能性方面,如果我们在50年前就把GOTO作为一个选项,我们今天可能还在与意大利面条代码作斗争。

为了获得纯函数式编程语言的全部好处,你不能妥协。你需要使用从一开始就按照这些原则设计的语言。只有采用它们,你才能获得我在这里列出的许多好处。

但是函数式编程并不是一帆风顺,它需要付出代价。学习按照函数式编程范式编程几乎就像从头开始学习编程一样。在很多情况下,开发人员必须熟悉他们在学校没有学过的数学。所需的数学并不难,只是新颖,对于数学恐惧症患者来说,有点吓人。

更重要的是,开发人员需要学习一种新的思维方式。起初这将是一个负担,因为他们不习惯它。但随着时间的推移,这种新的思维方式将成为第二本能,并最终减少认知开销与旧的思维方式相比。结果是效率的巨大收益。

但是向函数式编程过渡可能很困难,几年前我自己的经历就是一个例子。

我决定学习 Haskell,并且需要在业务时间表上完成,这是我40年职业生涯中最困难的学习经历,很大程度上是因为没有明确的资源来帮助开发人员过渡到函数式编程,事实上,在过去的30年里,没有人写过任何关于函数式编程的全面的东西。

我被迫从这里、那里、以及任何地方收集零碎的信息。我可以证明这个过程的效率非常低。我花了三个月的时间,白天、晚上和周末,每天都生活和呼吸着Haskell。但最终,我达到了这样一个观点:用Haskell写出来的代码比用其他任何东西写出来的都好。

当我决定我们的公司应该转向使用函数式语言时,我不想让我的开发人员经历同样的噩梦。因此,我开始为他们建立一个课程,这成为一本旨在帮助开发人员过渡到函数式程序员的书的基础。在我的书中,我为获得一种名为 PureScript 的函数式语言的熟练程度提供了指导,它继承了 Haskell 的所有优点,并改进了它的许多缺点。此外,它能够在浏览器和后端服务器中操作,使其成为当今许多软件需求的绝佳解决方案。

虽然这样的学习资源只能提供帮助,但要广泛地实现这种转变,基于软件的企业必须在他们最大的资产——开发人员身上投入更多。在我的公司Panoramic Software,我是首席技术官,我们已经进行了这种投资,所有新工作都用PureScript或Haskell来完成。

三年前,我们开始采用函数式语言,从另一种纯函数式语言Elm开始,因为它是一种更简单的语言。(我们当时几乎不知道我们最终会超越它。) 我们花了大约一年的时间才开始获得好处。但自从我们克服了这个困难,就很棒了。我们没有生产运行时错误,这在我们以前使用JavaScript的前端和Java后端时非常常见。这种改进使团队能够花更多的时间为系统添加新功能。现在,我们几乎不用花时间调试生产问题。

但是,当使用一种相对较少使用的人使用的语言时,仍然存在挑战,特别是缺乏在线帮助、文档和示例代码。很难雇佣有这些语言经验的开发人员。因此,我的公司使用专门寻找函数式编程人员的招聘人员。当我们雇佣没有函数式编程背景的人时,我们在头几个月让他们通过培训过程,使他们赶上进度。


函数式编程的未来
 

我的公司很小。它为政府机构提供软件,使他们能够帮助退伍军人从美国退伍军人事务部获得福利。这是一项非常有益的工作,但它不是一个利润丰厚的领域。由于利润微薄,我们必须使用所有可用的工具,以更少的开发人员做更多的工作。因此,函数式编程就是唯一的出路。

像我们这样平凡的企业很难吸引开发人员,这是很常见的。但我们现在能够雇佣顶级人才,因为他们想要在一个功能性的代码库上工作。走在这一趋势的前沿,我们可以得到大多数我们这个规模的公司只能梦想的人才。

我预计纯函数式语言的采用将提高整个软件行业的质量和健壮性,同时大大减少浪费在函数式编程中根本不可能产生的 bug 上的时间。这不是魔法,但有时感觉是那样的,每次我被迫使用非函数式代码库时,我都会想起我有多么好。

软件行业正在准备范式转移的一个迹象是函数式特性正在越来越多的主流语言中出现,要使行业完全过渡还需要更多的工作,但这样做的好处是显而易见的,毫无疑问,这是事情发展的方向。

 
欢迎关注公众号:清晰编程,获取更多精彩内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值