分享

好书丨为什么函数式编程最近才崛起

 星光闪亮图书馆 2018-06-17


小贴士


参与文末话题讨论,有机会获得赠书~


导语:函数式编程并不是新概念,实际上,在计算机科学出现之初,函数式编程就已经崭露头角,教科书式的函数式编程语言LISP在1958年就诞生了,但是,为什么一直都是命令式编程和面向对象编程大行其道呢?


函数式编程历史

说来话长,想当年,阿兰·图灵和冯·诺依曼祖师爷开天辟地,创立了计算机这门学科,因为这行前无古人,所以最早的一批学者都有其他专业的背景,有的是电子电气方面的专家,有来自物理学科,还有的本来是数学家。不同的背景,也就带来了对计算机发展方向的不同观点。


当时,数学家们提出的编程语言模型自然具有纯数学的气质,也是最优雅、最易于管理的解决方法。这其中的代表人物就是阿隆佐·邱奇(Alonzo Church),邱奇在计算机诞生之前就提出Lambda演算(Lambda Calculus)的概念,也就是用纯函数的组合来描述计算过程。根据Lambda演算,如果一个问题能够用一套函数组合的算法来描述,那就说明这个问题是可计算的,很自然,也就可以用编程语言的函数组合的方式来实现这样的计算过程。


可是,数学家们的理念,并没有在计算机发展初期被大范围应用,为什么呢?因为当时的硬件制造技术还很不发达,电子元件远没有当今这样的水平,那时候每一个电子元件制造成本高,而且体积大,无法在一小片芯片上放置很多元件,无论是运算元件还是存储元件,都是又慢又贵。


既然物理硬件昂贵,那么只好省着点用了,这种情况下,和硬件靠得最近的物理学家和电子电气工程师们掌握了编程语言的主流方向,命令式编程就是这样发展起来的。


早期的编程工作中,程序员必须考虑硬件的架构,如何使用CPU计算资源,如何巧妙利用有限的那么几个寄存器(register),如果不考虑的话,性能肯定无法过关。在这样的硬件条件下,函数式编程的想法要实现,只能通过一层软件模拟来复现数学家设想的模型,这多出来的一层无疑要耗费性能,所以光是性能这一个因素,就让函数式编程的实践难以推广。


还好,电子技术在飞速发展,计算机的运算能力和存储能力不断提高。1965年,电子芯片公司Intel的创始人戈登·摩尔根据观察,做了这样的断言:“当价格不变时,集成电路上可容纳的元器件的数目,约每隔18~24个月便会增加一倍,性能也将提升一倍。”这也就是著名的“摩尔定律”,根据这个定律,计算机的计算能力是以指数趋势增长,从那之后很长一段时间,软件行业也一直在享受计算能力增长带来的红利。


但是,进入21世纪之后,大家发现“摩尔定律”渐渐不管用了,集成电路上的元器件数目不能增长得这么快,因为,电子部件的密度快要达到物理极限了,一个集成电路上没法聚集更多的器件,虽然工程师们还在进一步提高CPU的性能,但是,普遍认同的观点是,单核的运算能力不可能保持摩尔定律的增长速度。


这时候,芯片的发展方向转为多核,软件架构也向分布式方向发展。这种转化很合理,既然一个核一秒钟只能做N次运算,那么我用8个核,一秒就能进行8*N次运算;同样,一个CPU中核的数量虽然是有限的,但是可以把计算量分布在不同的计算机上,假如一台计算机一秒钟的运算能力是N,那么1000台计算机,一秒钟的计算能力就是1000*N。


既然硬件的解决方案只能如此,剩下的唯一问题就是,如何把运算分布到不同的核或者不同的计算机上去呢?如果是用命令式编程,真的很难,因为编写协调多核或者分布式的任务处理程序非常困难,让每个开发者都做这样的工作,那真是非常不现实;然而,函数式编程却能够让大部分开发者不需要操心任务处理,所以非常适合分布式计算的场景。


声明式的函数,让开发者只需要表达“想要做什么”,而不需要表达“怎么去做”,这样就极大地简化了开发者的工作。至于具体“怎么去做”,让专门的任务协调框架去实现,这个框架可以灵活地分配工作给不同的核、不同的计算机,而开发者不必关心框架背后发生了什么。


与此同时,计算机业界也发现,随着CPU性能和存储设备性能的提高,当初导致函数式编程性能问题的障碍,现在都不是问题了,这也给函数式编程崛起增加了助推力。


可能读者会问,基于现在的计算机硬件架构,用函数式编程写出的程序,肯定性能会比命令式编程写出来的要低一些吧?其实未必。首先,当今软件已经是一个很复杂的系统,性能并不完全由是否更直接翻译为机器语言决定。其次,在性能相当的情况下,软件的开发速度要比运行速度重要得多。打个比方,用命令式编程,开发一个网络服务花费6个月,每个请求处理时间是1毫秒;用函数式编程,开发同样的网络服务花费3个月,每个请求处理时间是10毫秒,是否值得花3个月去获得这9毫秒的性能增长呢?


要知道,从客户端感知到的反应速度,不光包含服务器端的计算处理时间,还包含网络传输时间,比如平均网络传输时间是200毫秒,200毫秒是一个比较正常的网络延迟,那么访问命令式编程服务的反应时间是201毫秒,访问函数式编程服务的反应时间是210毫秒。201毫秒和210毫秒,用户感知的性能没有那么大的区别,这时候,我们当然更愿意选择能够提高开发速度的方法。


语言演进


除了硬件性能和软件开发需求的推动,语言的演进也是推动函数式编程被接受的一大动因。


曾几何时,只有Haskell和LISP这样的纯函数式编程语言才高举这面大旗,但是后来,一些本来属于命令式阵营或者面向对象阵营的编程语言也开始添加函数式的特性,这样,更多的开发者能够接触到函数式这种编程思想。增加了函数式特性的这些语言,当然包括JavaScript。


如果要学习最正统的函数式编程,比如Haskell,可能需要极大的耐心,因为学习过程中要涉及很多数学概念和推演,在开始实践函数式编程之前,就要面对这么庞大的背景知识,很有可能学着学着就睡着了。在《深入浅出RxJS》中,不会灌输给读者繁复的数学理论,而是尽量用浅显易懂的语言和代码示例来介绍函数式编程,空有理论没有实践是无意义的。


函数式编程和面向对象编程的比较


要介绍函数式编程(Functional Programming)就不得不拿另一个编程范式面向对象编程(Object Oriented Programming)作对比,因为面向对象编程曾经统治了业界很长一段时间,而函数式编程正在逐渐挑战面向对象的地位。


这两种编程方式都可以让代码更容易理解,不过方式不同。简单说来,面向对象的方法把状态的改变封装起来,以此达到让代码清晰的目的;而函数式编程则是尽量减少变化的部分,以此让代码逻辑更加清晰。


面向对象的思想是把数据封装在类的实例对象中,把数据藏起来,让外部不能直接操作这些对象,只能通过类提供的实例方法来读取和修改这些数据,这样就限制了对数据的访问方式。对于毫无节制任意修改数据的编程方式,面向对象无疑是巨大的进步,因为通过定义类的方法,可以控制对数据的操作。


但是,面向对象隐藏数据的特点,带来了一个先天的缺陷,就是数据的修改历史完全被隐藏了。有人说,面向对象编程提供了一种持续编写烂代码的方式,它让你通过一系列补丁来拼凑程序。这话有点过激,但是也道出了面向对象编程的缺点。


当我们在代码中看到一个对象实例的时候,即使知道了对象的当前状态,也没法知道这个对象是如何一步一步走到这个状态的,这种不确定性导致代码可维护性下降。


函数式编程中,倾向于数据就是数据,函数就是函数,函数可以处理数据,也是并不像面向对象的类概念一样把数据和函数封在一起,而是让每个函数都不要去修改原有数据(不可变性),而且通过产生新的数据来作为运算结果(纯函数)。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多