虫虫首页| 资源下载| 资源专辑| 精品软件
登录| 注册

您现在的位置是:首页 > 技术阅读 >  函数式编程减少漏洞的新方法

函数式编程减少漏洞的新方法

时间:2024-02-08
你可能认为软件产品生命周期中耗时最长、费用最高的阶段是系统的初期开发阶段,因为所有美妙的功能都是在这一阶段构想出来的。而事实上,最困难的部分是后期的维护阶段。在这个阶段,程序员将为自己在开发过程中走的捷径付出代价。那么,程序员为什么要走捷径?可能性有很多:也许他们没有意识到自己在“投机取巧”;只有代码被许多用户部署并执行时,隐藏的漏洞才会暴露出来;开发人员时间紧,也可能导致缺陷;此外,产品上市时间的压力几乎肯定会让软件包含更多的错误。
大多数公司维护代码的难题导致了第二个问题:脆弱性。添加到代码中的每个新功能都会增加代码的复杂性,从而增加程序中断的机会。软件变得越来越复杂,开发人员因为害怕出现程序中断,如非绝对必要,都尽量避免改动软件,这是很普遍的现象。在许多公司中,整个开发团队的工作不是为了做任何新的开发,而只是为了保持现有系统的运行。你可能会说,这就像是软件版本的红皇后效应,奋力奔跑只是为了停在原地。
这种现状令人遗憾。然而,软件行业目前的发展趋势就是复杂度越来越高、产品开发时间越来越长、运行系统的脆弱性越来越高。公司一般只能投入更多人力来解决这些问题:更多的开发人员、更多的测试人员、更多的技术人员在发现系统漏洞时及时干预。
当然,一定有更好的方法。越来越多的开发人员认为这一问题的答案可能是函数式编程,我便是其中的一员。本文中,我描述了什么是函数式编程,使用函数式编程为什么会有帮助,以及我为何如此热衷于函数式编程。
为更好地理解函数式编程的基本原理,我们先回顾半个多世纪前发生的事情。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作为一个选项,我们可能至今仍面临着“意大利面代码”的困境。
要获得纯函数式编程语言的全部好处,就不能妥协。需要使用从一开始就符合这些原则设计的语言。只有这样,才能获得本文阐述的许多益处。

但函数式编程并非易事,要有所付出。学习使用此类函数范式编程几乎就像从头再学编程一样。许多情况下,开发人员必须学习那些学校里不曾教过的数学知识。所需的数学并不难,但是新知识,而且对于数学恐惧症人群来说很可怕。
更重要的是,开发人员需要学习一种新的思维方式。因为不熟悉,起初这会让人感到有负担。但随着时间的推移,新的思维方式习惯成自然,与旧的思维方式相比,最终减少了认知成本,效率也就会大幅提升。
但实现到函数式编程的转变会很困难。我自己几年前的经历就很能说明问题。
我当时决定学习Haskell,并且要在业务要求的时间线内完成。在我40年的职业生涯中,这是最为艰难的学习经历,在很大程度上是因为缺少明确的资源来帮助开发人员实现到函数式编程的过渡。事实上,在过去30年里,没有人非常综合地写过函数式编程。
我只能到处拼拼凑凑。我个人的经验证明这样做的效率极低。我花了3个月时间,没日没夜、周末无休,时时刻刻都在学习Haskell。最后,我用它写的代码比用其他的都好。
在决定公司改用函数式语言时,我不想让公司的开发人员经历同样的噩梦。因此,我开始着手设计一个供开发人员使用的课程,并在此基础上编写一本书,帮助开发人员过渡到函数式编程。在书中,我指导读者熟练掌握一种名为PureScript的函数式语言,它吸收了Haskell的所有优点,并改进了Haskell的许多缺点。此外,它能够在浏览器和后台服务器上运行,成为满足当今许多软件需求的绝佳解决方案。
这些学习资源只能起到帮助作用,要广泛实现这种转变,软件型企业必须对其最大的资产——开发人员——进行更多投资。我在自己的公司Panoramic Software担任首席技术官,公司进行了此项投资,所有新工作都用PureScript或Haskell完成。
3年前我们开始采用函数式语言时,使用的是另一种名为Elm的纯函数式语言,因为它是一种更简单的语言。(我们全然不知最终能够超越它。)我们花了近一年的时间才开始有所收获。但自从我们渡过难关之后,一路顺畅无比。我们没有产生运行错误,过去在前端用JavaScript、在后端用Java时,运行错误非常常见。这一改进让团队能够把更多的时间花在为系统添加新功能上。现在,我们几乎不需要花时间调试生产问题。
但是,对于相对较少有人使用的语言来说,仍然存在挑战,例如缺少在线帮助、文档和示例代码。也很难聘请具有相关语言经验的开发人员。正因如此,我们公司的招聘人员专门寻找函数式程序员。如果雇用的人没有函数式编程背景,我们会在最初的几个月里让他们接受培训,以便跟上进度。
我的公司很小,它向政府机构提供软件,帮助退伍军人获取美国退伍军人事务部的福利。这项工作非常有意义,但它并非赚大钱的领域。由于利润微薄,我们必须使用所有的可用工具,用更少的开发人员做更多的事。函数式编程正是我们所需要的。
像我们这样平淡无奇的企业,通常很难吸引开发人员。但我们现在能够雇用顶级人才,因为他们想从事函数式代码库工作。我们在这一领域所处的领先地位,让我们可以获得我们这种规模的公司梦寐以求的人才。
我预计,采用纯函数式语言将提高整个软件行业的质量和可靠性,同时大大减少浪费在查找漏洞上的时间,函数式编程基本不产生漏洞。这不是什么魔法,但有时它很神奇,每当被迫使用非函数式代码库时,我都会想起有它多好。
整个软件行业正在为范式转变作准备,其中一个迹象便是函数功能正出现在越来越多的主流编程语言中。软件行业需要做更多的工作才能完全实现过渡,但这样做的好处显而易见,毫无疑问,这也是未来的发展方向。

作者:Charles Scalfani

IEEE Spectrum

《科技纵览》

官方微信公众平台






往期推荐
2047年的晶体管
晶体管终极时间轴
将摩尔定律推向新高度