什么是函数式编程?使用函数式编程为什么会有帮助呢?,函数式编程

描述

你可能认为软件产品生命周期中耗时最长、费用最高的阶段是系统的初期开发阶段,因为所有美妙的功能都是在这一阶段构想出来的。而事实上,最困难的部分是后期的维护阶段。在这个阶段,程序员将为自己在开发过程中走的捷径付出代价。那么,程序员为什么要走捷径?可能性有很多:也许他们没有意识到自己在“投机取巧”;只有代码被许多用户部署并执行时,隐藏的漏洞才会暴露出来;开发人员时间紧,也可能导致缺陷;此外,产品上市时间的压力几乎肯定会让软件包含更多的错误。

大多数公司维护代码的难题导致了第二个问题:脆弱性。添加到代码中的每个新功能都会增加代码的复杂性,从而增加程序中断的机会。软件变得越来越复杂,开发人员因为害怕出现程序中断,如非绝对必要,都尽量避免改动软件,这是很普遍的现象。在许多公司中,整个开发团队的工作不是为了做任何新的开发,而只是为了保持现有系统的运行。你可能会说,这就像是软件版本的红皇后效应,奋力奔跑只是为了停在原地。

这种现状令人遗憾。然而,软件行业目前的发展趋势就是复杂度越来越高、产品开发时间越来越长、运行系统的脆弱性越来越高。公司一般只能投入更多人力来解决这些问题:更多的开发人员、更多的测试人员、更多的技术人员在发现系统漏洞时及时干预。

当然,一定有更好的方法。越来越多的开发人员认为这一问题的答案可能是函数式编程。本文中,我描述了什么是函数式编程,使用函数式编程为什么会有帮助,以及我为何如此热衷于函数式编程。

为更好地理解函数式编程的基本原理,我们先回顾半个多世纪前发生的事情。20世纪60年代后期,为了提高代码质量,减少所需的开发时间,一种编程范式应运而生,称为结构化编程。

各种语言的出现促进了结构化编程的发展,为了更好地支持结构化编程,一些已有的语言被修改。结构化编程语言最显著的特征之一,是消除了一个长期存在的特征:GOTO语句。

GOTO语句用于程序执行的重新定向。程序流不是按顺序执行下一条语句,而是重定向至其他某个语句,即GOTO行中指定的语句,通常需满足某些条件。

取消GOTO语句是基于程序员在使用GOTO的过程中学到的教训——它让程序非常难以理解。带有GOTO语句的程序通常被称为“意大利面代码”,因为指令序列执行可能就像一碗意大利面,难以单链跟随。

开发人员无法理解自己的代码是如何工作的,或者为什么代码有时不工作,这是一个复杂的问题。那个时代的软件专家认为,GOTO语句造成了不必要的复杂性,因此必须消除这些GOTO语句。

这在当时是颇为激进的想法,许多程序员拒绝消除自己一直依赖的语句。相关争论持续了十多年,最终,GOTO不存在了,今天也没人主张它再次回归。这是因为在高级编程语言中消除GOTO语句大大降低了复杂度,提高了生产软件的可靠性。这是通过对程序员的限制实现的,其结果是程序员更容易推理自己编写的代码。

尽管软件行业已经从现代高级语言中消除了GOTO语句,但软件的复杂度和脆弱性仍在继续上升。如果想看看还能修改哪些编程语言以避开一些常见的陷阱,你会很奇怪地发现,软件设计师往往能在硬件同行那里找到灵感。

在设计计算机硬件时,电阻不能共用,比如键盘和显示器电路就不能共用电阻。但程序员在软件中却一直在做这种共用,也就是全局状态共享:变量不由某一个进程所有,而可由任意数量的进程进行更改,甚至可以同时更改。

现在,想象一下,你每次使用微波炉时,洗碗机的循环设置会从一般程序变为瓶罐清洗程序。当然,这在现实世界中并不会发生,但在软件中,这样的情况却一直出现。程序员编写调用一个函数的代码,期望执行单个任务。但是许多函数都有副作用,会改变共享的全局状态,从而导致意想不到的后果。

在硬件中,这种情况不会发生,因为物理定律限制了这种可能性。当然,硬件工程师也可能会搞砸,但不像软件那样有太多的可能,且有好有坏。

另一个潜藏在软件“沼泽”中的复杂怪物被称为空引用,即引用内存中某个位置根本不指向任何内容。一旦尝试使用此引用,就会出现错误。因此,程序员必须牢记,在尝试读取或更改引用的内容前,需检查该引用是否为空。

当今几乎所有流行的语言都存在这一缺陷。先驱计算机科学家托尼•霍尔(Tony Hoare)早在1965年就在ALGOL语言中引入了空引用,空引用后来被纳入许多其他语言。霍尔解释说,自己这样做“仅仅是因为它很容易实施”,但今天他认为这是一个“数十亿美元的错误”。因为当程序员期望的是有效引用而实际上是空引用时,便会导致无数错误。

软件开发人员需要非常自律,才能避免此类陷阱,但有时他们没有采取足够的预防措施。结构化编程的架构师知道GOTO语句确实是陷阱,未给开发人员留下任何逃避的借口。为保证无GOTO语句的代码获得预期的清晰度改善,他们知道必须在结构化编程语言中完全消除GOTO语句。

历史证明,删除危险特征可大大提高代码的质量。今天,许多危险的习惯做法损害了软件的鲁棒性和可维护性。几乎所有现代编程语言均有某种形式的空引用、全局状态共享和带有副作用的函数,这些要比GOTO语句糟糕得多。

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

第一个流行的纯函数式语言称为Haskell,创建于1990年。因此,软件开发领域如今依旧面临的棘手问题早在20世纪90年代中期便已有了解决方案。遗憾的是,当时的硬件通常不够强大,无法使用该解决方案。但今天的处理器已经能够轻松管理Haskell和其他纯函数式语言的需求。

事实上,基于纯函数的软件特别适合现代多核CPU。这是因为纯函数仅靠输入参数运行,因而不同函数间不可能存在交互。这使我们可以对编译器进行优化,生成在多个内核上高效、轻松运行的代码。

顾名思义,纯函数式编程意味着开发人员只能编写纯函数,既然是纯函数,便不会产生副作用。这种限制提高了稳定性,打开了编译器优化的大门,最终生成的代码更容易推理。

但若是函数需要知道或操作某个系统的状态,又该如何?这种情况下,状态会由一长串“组合函数”进行传递——一个函数将其输出传递给下一个函数作为输入。将状态自一个函数传递至另一个函数,每个函数都可以访问该状态,且不会出现另一个并发程序线程对该状态进行修改——这是在太多程序中发现的常见且代价高昂的脆弱性。

函数式编程亦可解决霍尔的“十亿美元错误”:空引用。解决的方法是不允许值为空。另外,有一种结构通常称为Maybe(或某些语言中的Option)。Maybe的值可以是Nothing或Just。使用Maybe结构,开发人员不得不始终考虑这两种情况。在这件事上他们别无选择,每一次遇到Maybe时都必须处理Nothing的情况。这样做可以消除空引用可能造成的许多错误。

函数式编程还要求数据不可变,这意味着一旦将变量设置为某个值,该值就永远不变。变量更像是数学中的变量。

例如,要计算方程y= x2 + 2x - 11,需要为x选择一个值,并且在计算y的过程中,x都不会取不同的值。因此,计算x2时使用的x值与计算2 x时所用的x值是相同的。在大多数编程语言中没有这样的限制。可以使用一个值计算x2,然后在计算2 x之前更改x的值。不允许开发人员将赋值更改(变异),他们可以使用与中学代数课相同的推理过程。

与大多数语言不同,函数式编程语言深深植根于数学。这种逻辑极为严密的数学血统正是函数式语言最大的优势。

为何是这样?因为人们研究数学的历史已有数千年之久。它牢不可破。而大多数编程范式(如面向对象的编程)背后的历史最多只有60年,相比之下显得粗糙且不成熟。

不妨通过一个例子来说明编程与数学相比有多“草率”。通常情况下,我们会告诉编程新手在第一次遇到语句x = x + 1时忘记自己在数学课上学的东西。在数学中,这个方程为零解。但在当今的大多数编程语言中,x = x + 1不是一个等式。它是一个语句,命令计算机读取x的值,将其加1后,放回名为x的变量中。

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

由于函数的纯粹性,我们可以使用代数替换来推理代码,从而帮助降低代码复杂性,就像回到代数课上,降低方程复杂性一样。在非函数式语言(命令式语言)中,则并无同等机制来推理代码是如何工作的。

 纯函数式编程删除了编程语言中的危险特征,解决了我们行业中的许多大问题,开发人员也不容易出现“搬起石头砸自己的脚”的问题。这些限制起初可能看来很极端,我可以肯定地说,20世纪60年代开发人员对消除GOTO也有相同感。但事实是,使用函数式语言既不失自由,功能又强大,以至于当今几乎所有最流行的语言都包含了函数功能,尽管它们本质上仍然是命令式语言。

这种混和编程方法的最大问题在于它仍然允许开发人员忽略语言的函数性质。如果50年前保留GOTO作为一个选项,我们可能至今仍面临着“意大利面代码”的困境。

要获得纯函数式编程语言的全部好处,就不能妥协。需要使用从一开始就符合这些原则设计的语言。只有这样,才能获得本文阐述的许多益处。

但函数式编程并非易事,要有所付出。学习使用此类函数范式编程几乎就像从头再学编程一样。许多情况下,开发人员必须学习那些学校里不曾教过的数学知识。所需的数学并不难,但是新知识,而且对于数学恐惧症人群来说很可怕。

更重要的是,开发人员需要学习一种新的思维方式。因为不熟悉,起初这会让人感到有负担。但随着时间的推移,新的思维方式习惯成自然,与旧的思维方式相比,最终减少了认知成本,效率也就会大幅提升。

 




审核编辑:刘清

相关推荐

相关文章