函数式编程另类指南.docx

上传人:b****6 文档编号:4062173 上传时间:2022-11-27 格式:DOCX 页数:17 大小:36.66KB
下载 相关 举报
函数式编程另类指南.docx_第1页
第1页 / 共17页
函数式编程另类指南.docx_第2页
第2页 / 共17页
函数式编程另类指南.docx_第3页
第3页 / 共17页
函数式编程另类指南.docx_第4页
第4页 / 共17页
函数式编程另类指南.docx_第5页
第5页 / 共17页
点击查看更多>>
下载资源
资源描述

函数式编程另类指南.docx

《函数式编程另类指南.docx》由会员分享,可在线阅读,更多相关《函数式编程另类指南.docx(17页珍藏版)》请在冰豆网上搜索。

函数式编程另类指南.docx

函数式编程另类指南

函数式编程另类指南

原文:

FunctionalProgrammingForTheRestofUs

原文作者:

VyacheslavAkhmechet

翻译:

lihaitao(电邮:

lihaitao在)

程序员拖沓成性,每天到了办公室后,泡咖啡,检查邮箱,阅读RSSfeed,到技术站点查阅最新的文章,在编程论坛的相关版面浏览公共讨论,并一次次地刷新以免漏掉一条信息。

然后是午饭,回来后盯了IDE没几分钟,就再次检查邮箱,倒咖啡。

最后在不知不觉中,结束了一天。

不平凡的事是每隔一段时间会跳出一些很有挑战性的文章。

如果没错,这些天你至少发现了一篇这类文章——很难快速通读它们,于是就将之束之高阁,直到突然你发现自己已经有了一个长长的链接列表和一个装满了PDF文件的目录,然后你梦想着到一个人迹罕至的森林里的小木屋苦读一年以期赶上,要是每天清晨你沿着那里的林中小溪散步时会有人带来食物和带走垃圾就更好了。

虽然我对你的列表一无所知,但我的列表却是一大堆关于函数式编程的文章。

而这些基本上是最难阅读的了。

它们用枯燥的学院派语言写成,即使“在华尔街行业浸淫十年的专家(veterans)”也不能理解函数式编程(也写作FP)都在探讨些什么。

如果你去问花旗集团(CitiGroup)或德意志银行(DeutscheBank)的项目经理[1],为什么选择了JMS而不Erlang,他们可能回答不能在产业级的应用中使用学院派语言。

问题是,一些最为复杂的,有着最严格需求的系统却是用函数式编程元素写成。

有些说法不能让人信服。

的确,关于函数式编程的文章和论文难于理解,但他们本来不必这么晦涩。

这一知识隔阂的形成完全是历史原因。

函数式编程的概念本身并不困难。

这篇文章可以作为“简易的函数式编程导引”。

是一座从我们命令式(imperative)的思维模式到函数式编程的桥梁。

去取杯咖啡回来继续读下去吧。

可能你的同事很快就会开始取笑你对函数式编程发表的观点了。

那么什么是函数式编程呢?

它怎么产生?

它可以被掌握吗(Isitedible)?

如果它真如其倡导者所言,为什么没有在行业中得到更广泛的使用?

为什么好像只有那些拿着博士学位的人才使用它?

最要紧的是,为什么它就TMD这么难学?

这些closure,continuation,currying,惰性求值和无副作用等等究竟是些什么东西?

没有大学参与的项目怎么使用它?

为什么它看上去这么诡异于和我们命令式思想友好,圣洁和亲近的一切的一切?

我们将于不久扫清这些疑问。

首先让我来解释形成实际生活和学界文章之间巨大隔阂的缘起,简单得像一次公园的散步。

信步游园

启动时间机器,我们散步在两千多年以前的一个被遗忘了太久的春季明媚的日子,那是公元前380年。

雅典城墙外的橄榄树树荫里,柏拉图和一个英俊的奴隶小男孩朝着学院走去。

“天气真好”,“饮食不错”,然后话题开始转向哲思。

“瞧那两个学生,”为了使问题更容易理解,柏拉图仔细地挑选着用词,“你认为谁更高呢?

小男孩看着那两个人站着的水漕说,“他们差不多一样高”。

柏拉图说:

“你的差不多一样是什么意思?

”。

“我在这里看他们是一样高的,不过我肯定如果走近些就会看出他们高度的差别。

柏拉图笑了,他正把这个孩子带到正确的方向。

“那么你是说,我们这个世界没有完全的等同了?

小男孩想了一会儿回答,“对,我不这样认为,任何事物总有一些区别,即使我们看不到它。

这句话非常到位!

“那么如果这世上没有完全的相等,你又是如何理解‘完全’相等这个概念的呢?

小男孩迷惑得说:

“我不知道。

”最初尝试着理解数学的本源(nature)时也会产生这种疑惑。

柏拉图暗示这个世上的万物都只是一个对完美的近似。

他还认识到我们即使没有接触到完美但依然可以理解这一概念。

所以他得出结论,完美的数学形式只能存在于另一个世界,我们通过和那个世界的某种联系在一定程度上知晓他们。

很明显我们不能看到完美的圆,但我们可以理解什么是完美的圆并用数学公式将它表达出来。

那么,什么是数学?

为什么宇宙可以用数学定理描述?

数学可以描述宇宙中的所有现象吗?

[2]

数学哲学是一个很复杂的课题。

像大多数哲学学科一样它更倾向于提出问题而不是给出解答。

这些意见中很多都循回绕转于一个事实,即数学实际上是一个谜语:

我们设置了一系列基本的不冲突的原理和一些可以施加于这些原理的操作规则,然后我们就能堆砌这些规则以形成更复杂的规则。

数学家把这种方法叫做“形式系统”或“演算”。

如果愿意,我们可以很快写出一个关于Tetris(译者注:

一种通常被称为俄罗斯方块的游戏)的形式系统。

实际上,工作中的Tetris实现就是一个形式系统,只是被指定使用了个不常见的表现形式。

人马座的那个生物文明也许不能理解我们的Tetris和圆的范式,因为可能他们唯一的感知输入是气味香橙的橘子。

他们也许永远不会发现Tetris范式,但很可能会有一个圆的范式。

我们也可能将无法阅读它,因为我们的嗅觉没有那么复杂,可是一旦我们理解(pass)了那一范式的表示形式(通过这种传感器和标准解码技术来理解这种语言),其底层的概念就可被任何智能文明所理解。

有趣的是如果从来没有智能文明存在,Tetris和圆的范式仍然严密合理,只是没有人注定将会发现他们。

如果产生了一种智能文明,他就会发现一些形式系统来帮助描述宇宙的规律。

但他还是不大可能发现Tetris因为宇宙中再没有和它相似的事物。

在现实世界中这类无用的形式系统或迷题的例子数不胜数,Tetris只是其中的一个典型。

我们甚至不能确定自然数是否是对客观世界的完整近似,至少我们可以简单的想像一个很大的数它不能用宇宙中任何东西描述,因为它以近乎无穷。

历史一瞥[3]

再次启动时间机器,这一次的旅行近了很多,我们回到1930年代。

大萧条正在蹂躏着那个或新或就的时代。

空前的经济下挫影响着几乎所有阶层的家庭生活,只有少数人还能够保持着饥谨危机前的安逸。

一些人就如此幸运地位列其中,我们关心的是普林斯顿大学的数学家们。

采用了歌特式风格设计建造的新办公室给普林斯顿罩上天堂般的幸福光环,来自世界各地的逻辑学家被邀请到普林斯顿建设一个新的学部。

虽然彼时的美国民众已难能弄到一餐的面包,普林斯顿的条件则是可以在高高的穹顶下,精致雕凿的木质墙饰边上整日的品茶讨论或款款慢步于楼外的林荫之中。

阿隆左·丘奇就是一个在这种近于奢侈的环境中生活着的数学家。

他在普林斯顿获得本科学位后被邀留在研究生院继续攻读。

阿隆左认为那里的建筑实属浮华,所以他很少一边喝茶一边与人讨论数学,他也不喜欢到林中散步。

阿隆左是一个孤独者:

因为只有一个人时他才能以最高的效率工作。

虽然如此,他仍与一些普林斯顿人保持的定期的联系,其中包括阿兰·图灵,约翰·冯·诺依曼,和kurtGrodel。

这四个人都对形式系统很感兴趣,而不太留意现实世界,以便致力于解决抽象的数学难题。

他们的难题有些共同之处:

都是探索关于计算的问题。

如果我们有了无限计算能力的机器,哪些问题可以被解决?

我们可以使他们自动地得以解决吗?

是否还是有些问题无法解决,为什么?

不同设计的各种机器是否具有相同的计算能力?

通过和其它人的合作,阿隆左·丘奇提出了一个被称为lambda演算的形式系统。

这个系统本质上是一种虚拟的机器的编程语言,他的基础是一些以函数为参数和返回值的函数。

函数用希腊字母lambda标识,这个形式系统因此得名[4]。

利用这一形式系统,阿隆左就可以对上述诸多问题推理并给出结论性的答案。

独立于阿隆左,阿兰·图灵也在进行着相似的工作,他提出了一个不同的形式系统(现在被称为图灵机),并使用这一系统独立得给出了和阿隆左相似的结论。

后来被证明图灵机和lambda演算能力等同。

我们的故事本可以到此结束,我会就此歇笔,而你也将浏览到下一个页面,如果第二次世界大战没有在那时打响。

整个世界笼罩在战争的火光和硝烟之中,美国陆军和海军前所未有的大量使用炮弹,为了改进炮弹的精确度,部队组织了大批的科学家持续地计算微分方程以解出弹道发射轨迹。

渐渐意识到这个任务用人力手工完成太耗精力后,人们开始着手开发各种设备来攻克这个难关。

第一个解出了弹道轨迹的机器是IBM制造的MarkI——它重达5吨,有75万个组件,每秒可以完成三次操作。

竞争当然没有就此结束,1949年,EDVAC(ElectronicDiscreteVariableAutomaticComputer,爱达瓦克)被推出并获得了极大的成功。

这是对冯·诺依曼架构的第一个实践实例,实际上也是图灵机的第一个现实实现。

那一年好运与阿隆左·丘奇无缘。

直到1950年代将尽,一位MIT的教授JohnMcCarthy(也是普林斯顿毕业生)对阿隆左·丘奇的工作产生了兴趣。

1958年,他公开了表处理语言Lisp。

Lisp是对阿隆左·丘奇的lambda演算的实现但同时它工作在冯·诺依曼计算机上!

很多计算机科学家认识到了Lisp的表达能力。

1973年,MIT人工智能实验室的一组程序员开发了被称为Lisp机器的硬件-阿隆左lambda演算的硬件实现!

函数式编程

函数式编程是对阿隆左·丘奇理论的实践应用。

但也并非全部lambda演算都被应用到了实践中,因为lambda演算不是被设计为在物理局限下工作的。

因此,象面向对象的编程一样,函数式编程是一系列理念,而不是严格的教条。

现在有很多种函数式编程语言,他们中的大多数以不同方式完成不同任务。

在本文中我将就最广泛使用的源自函数式编程的思想作一解释,并将用Java语言举例。

(的确,你可以用Java写出函数式的程序如果你有显著的受虐倾向)。

在下面的小节中,我将会把Java作为一种函数式语言,并对其稍加修改使它成为一种可用的函数式语言。

现在开始吧。

lambda演算被设计用来探询关于计算的问题,所以函数式编程主要处理计算,并惊人地用函数来完成这一过程。

函数是函数式编程的基本单位,函数几乎被用于一切,包括最简单的计算,甚至变量都由计算取代。

在函数式编程中,变量只是表达式的别名(这样我们就不必把所有东西打在一行里)。

变量是不能更改的,所有变量只能被赋值一次。

用Java的术语来说,这意味着所有单一变量都被声明为final(或C++的const)。

在函数式编程中没有非final的变量。

finalinti=5;

finalintj=i+3;

因为函数式编程中所有变量都是final的,所以可以提出这样两个有趣的表述:

没有必要总是写出关键字final,没有必要把变量再称为变量。

那么现在我们对Java作出两个修改:

在我们的函数式Java中所有变量默认都是final的,我们将变量(variable)称为符号(symbol)。

就此你也许会质疑,用我们新创造的语言还能写出有些复杂度的程序吗?

如果每个符号都是不可变更(non-mutalbe)的,那么就无法改变任何状态!

其实事实并非完全如此。

在阿隆左研究其lambda演算时,他并不想将某个状态维护一段时间以期未来对其进行修改。

他关注的是对数据的操作(也通常被称为”演算体caculatingstuff”)。

既然已被证明lambda演算与图灵机等价,它可以完成所有命令式编程语言能够完成的任务。

那么,我们怎么才能做到呢?

答案是函数式程序能保存状态,只是它并非通过变量而是使用函数来保存状态。

状态保存在函数的参数中,保存在堆栈上。

如果你要保存某个状态一段时间并时不时地对其进行一些修改,可以写个递归函数。

举个例子,我们写个函数来翻转Java的字符串。

记住,我们声明的每个变量默认都是final的。

[5]

Stringreverse(Stringarg){

if(arg.length==0){

returnarg;

}

else{

returnreverse(arg.substring(1,arg.length))+arg.substring(0,1);

}}

这个函数很慢因为它不断地调用自己[6],它还也是个嗜内存魔因为要持续分配对象。

不过它的确是在用函数式风格。

你可能会问,怎么有人会这样写程序?

好的,我这就慢慢讲来:

函数式编程的优点

你可能会认为我根本无法对上面那个畸形的函数给出个合理的解释。

我开始学习函数式编程时就是这么认为的。

不过我是错了。

有很好的理由使用这种风格,当然其中一些属主观因素。

例如,函数式程序被认为更容易阅读。

因为每个街区的孩子都知道,是否容易理解在旁观者的眼中,所以我将略去这些主观方面的理由。

幸运的是,还有很多的客观理由。

单元测试

因为函数式编程的每一个符号都是final的,没有函数产生过副作用。

因为从未在某个地方修改过值,也没有函数修改过在其作用域之外的量并被其他函数使用(如类成员或全局变量)。

这意味着函数求值的结果只是其返回值,而惟一影响其返回值的就是函数的参数。

这是单元测试者的梦中仙境(wetdream)。

对被测试程序中的每个函数,你只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态。

所有要做的就是传递代表了边际情况的参数。

如果程序中的每个函数都通过了单元测试,你就对这个软件的质量有了相当的自信。

而命令式编程就不能这样乐观了,在Java或C++中只检查函数的返回值还不够——我们还必须验证这个函数可能修改了的外部状态。

调试

如果一个函数式程序不如你期望地运行,调试也是轻而易举。

因为函数式程序的bug不依赖于执行前与其无关的代码路径,你遇到的问题就总是可以再现。

在命令式程序中,bug时隐时现,因为在那里函数的功能依赖与其他函数的副作用,你可能会在和bug的产生无关的方向探寻很久,毫无收获。

函数式程序就不是这样——如果一个函数的结果是错误的,那么无论之前你还执行过什么,这个函数总是返回相同的错误结果。

一旦你将那个问题再现出来,寻其根源将毫不费力,甚至会让你开心。

中断那个程序的执行然后检查堆栈,和命令式编程一样,栈里每一次函数调用的参数都呈现在你眼前。

但是在命令式程序中只有这些参数还不够,函数还依赖于成员变量,全局变量和类的状态(这反过来也依赖着这许多情况)。

函数式程序里函数只依赖于它的参数,而那些信息就在你注视的目光下!

还有,在命令式程序里,只检查一个函数的返回值不能够让你确信这个函数已经正常工作了,你还要去查看那个函数作用域外数十个对象的状态来确认。

对函数式程序,你要做的所有事就是查看其返回值!

沿着堆栈检查函数的参数和返回值,只要发现一个不尽合理的结果就进入那个函数然后一步步跟踪下去,重复这一个过程,直到它让你发现了bug的生成点。

并行

函数式程序无需任何修改即可并行执行。

不用担心死锁和临界区,因为你从未用锁!

函数式程序里没有任何数据被同一线程修改两次,更不用说两个不同的线程了。

这意味着可以不假思索地简单增加线程而不会引发折磨着并行应用程序的传统问题。

事实既然如此,为什么并不是所有人都在需要高度并行作业的应用中采用函数式程序?

嗯,他们正在这样做。

爱立信公司设计了一种叫作Erlang的函数式语言并将它使用在需要极高抗错性和可扩展性的电信交换机上。

还有很多人也发现了Erlang的优势并开始使用它。

我们谈论的是电信通信控制系统,这与设计华尔街的典型系统相比对可靠性和可升级性要求高了得多。

实际上,Erlang系统并不可靠和易扩展,Java才是。

Erlang系统只是坚如磐石。

关于并行的故事还没有就此停止,即使你的程序本身就是单线程的,那么函数式程序的编译器仍然可以优化它使其运行于多个CPU上。

请看下面这段代码:

Strings1=somewhatLongOperation1();

Strings2=somewhatLongOperation2();

Strings3=concatenate(s1,s2);

在函数编程语言中,编译器会分析代码,辨认出潜在耗时的创建字符串s1和s2的函数,然后并行地运行它们。

这在命令式语言中是不可能的,因为在那里,每个函数都有可能修改了函数作用域以外的状态并且其后续的函数又会依赖这些修改。

在函数式语言里,自动分析函数并找出适合并行执行的候选函数简单的像自动进行的函数内联化!

在这个意义上,函数式风格的程序是“不会过时的技术(futureproof)”(即使不喜欢用行业术语,但这回要破例一次)。

硬件厂商已经无法让CPU运行得更快了,于是他们增加了处理器核心的速度并因并行而获得了四倍的速度提升。

当然他们也顺便忘记提及我们的多花的钱只是用在了解决平行问题的软件上了。

一小部分的命令式软件和100%的函数式软件都可以直接并行运行于这些机器上。

代码热部署

过去要在Windows上安装更新,重启计算机是难免的,而且还不只一次,即使是安装了一个新版的媒体播放器。

WindowsXP大大改进了这一状态,但仍不理想(我今天工作时运行了WindowsUpdate,现在一个烦人的图标总是显示在托盘里除非我重启一次机器)。

Unix系统一直以来以更好的模式运行,安装更新时只需停止系统相关的组件,而不是整个操作系统。

即使如此,对一个大规模的服务器应用这还是不能令人满意的。

电信系统必须100%的时间运行,因为如果在系统更新时紧急拨号失效,就可能造成生命的损失。

华尔街的公司也没有理由必须在周末停止服务以安装更新。

理想的情况是完全不停止系统任何组件来更新相关的代码。

在命令式的世界里这是不可能的。

考虑运行时上载一个Java类并重载一个新的定义,那么所有这个类的实例都将不可用,因为它们被保存的状态丢失了。

我们可以着手写些繁琐的版本控制代码来解决这个问题,然后将这个类的所有实例序列化,再销毁这些实例,继而用这个类新的定义来重新创建这些实例,然后载入先前被序列化的数据并希望载入代码可以恰到地将这些数据移植到新的实例。

在此之上,每次更新都要重新手动编写这些用来移植的代码,而且要相当谨慎地防止破坏对象间的相互关系。

理论简单,但实践可不容易。

对函数式的程序,所有的状态即传递给函数的参数都被保存在了堆栈上,这使的热部署轻而易举!

实际上,所有我们需要做的就是对工作中的代码和新版本的代码做一个差异比较,然后部署新代码。

其他的工作将由一个语言工具自动完成!

如果你认为这是个科幻故事,请再思考一下。

多年来Erlang工程师一直更新着他们的运转着的系统,而无需中断它。

机器辅助的推理和优化

函数式语言的一个有趣的属性就是他们可以用数学方式推理。

因为一种函数式语言只是一个形式系统的实现,所有在纸上完成的运算都可以应用于用这种语言书写的程序。

编译器可以用数学理论将转换一段代码转换为等价的但却更高效的代码[7]。

多年来关系数据库一直在进行着这类优化。

没有理由不能把这一技术应用到常规软件上。

另外,还能使用这些技术来证明部分程序的正确,甚至可能创建工具来分析代码并为单元测试自动生成边界用例!

对稳固的系统这种功能没有价值,但如果你要设计心房脉冲产生器(pacemaker)或空中交通控制系统,这种工具就不可或缺。

如果你编写的应用程序不是产业的核心任务,这类工具也是你强于竞争对手的杀手锏。

高阶函数

我记得自己在了解了上面列出的种种优点后曾想:

“那都是非常好的特性,可是如果我不得不用天生就不健全的语言编程,把一切变量声明为

final产生的代码将是垃圾一堆。

”这其实是误解。

在如Java这般的命令式语言环境里,将所有变量声明为final没有用,但是在函数式语言里不是这样。

函数式语言提供了不同的抽象工具它会使你忘记你曾经习惯于修改变量。

高阶函数就是这样一种工具。

函数式语言中的函数不同于Java或C中的函数,而是一个超集——它有着Java函数拥有的所有功能,但还有更多。

创建函数的方式和C中相似:

intadd(inti,intj){

returni+j;

}

这意味着有些东西和同样的C代码有区别。

现在扩展我们的Java编译器使其支持这种记法。

当我们输入上述代码后编译器会把它转换成下面的Java代码(别忘了,所有东西都是final的):

classadd_function_t{

intadd(inti,intj){

returni+j;

}

}

add_function_tadd=newadd_function_t();

这里的符号add并不是一个函数。

这是一个有一个成员函数的很小的类。

我们现在可以把add作为函数参数放入我们的代码中。

还可以把它赋给另一个符号。

我们在运行时创建的add_function_t的实例如果不再被使用就将会被垃圾回收掉。

这些使得函数成为第一级的对象无异于整数或字符串。

(作为参数)操作函数的函数被称为高阶函数。

别让这个术语吓着你,这和Java的class操作其它class(把它们作为参数)没有什么区别。

我们本可以把它们称为“高阶类”但没有人注意到这个,因为Java背后没有一个强大的学术社区。

那么怎样,何时应该使用高阶函数呢?

我很高兴你这样问。

如果你不曾考虑类的层次,就可能写出了一整团堆砌的代码块。

当你发现其中一些行的代码重复出现,就把他们提取成函数(幸运的是这些依然可以在学校里学到)。

如果你发现在那个函数里一些逻辑动作根据情况有变,就把他提取成高阶函数。

糊涂了?

下面是一个来自我工作的实例:

假如我的一些Java代码接受一条信息,用多种方式处理它然后转发到其他服务器。

classMessageHandler{

voidhandleMessage(Messagemsg){

//…

msg.setClientCode(”ABCD_123″);

//…

sendMessage(msg);

}

//…

}

现在假设要更改这个系统,现在我们要把信息转发到两个服务器而不是一个。

除了客户端的代码一切都像刚才一样——第二个服务器希望这是另一种格式。

怎么处理这种情况?

我们可以检查信息的目的地并相应修改客户端代码的格式,如下:

classMessageHandler{

voidhandleMessage(Messagemsg){

//…

if(msg.getDestination().equals(”server1″){

msg.setClientCode(”ABCD_123″);

}else{

msg.setClientCode(”123_ABC”);

}

//…

sendMessage(msg);

}

//…

}

然而这不是可扩展的方法,如果加入了更多的服务器,这个函数将线性增长,更新它会成为我的梦魇。

面向对象的方法是把MessageHandler作为基类,在导出类中专业化客户代码操作:

abstractclassMessageHandler{

voidhandleMessage(Messagemsg){

//…

msg.setClientCode(getClientCode());

//…

sendMessage(msg);

}

abstractStringgetClientCode();

//…

}

classMessageHandlerOneextendsMessageHandler{

StringgetClientCode(){

return“ABCD_123″;

}

}

classMessageHandlerTwoextendsMessageHandler{

StringgetClientCode(){

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育 > 政史地

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1