收藏 分享(赏)

函数式编程另类指南.doc

上传人:czsj190 文档编号:4406983 上传时间:2018-12-27 格式:DOC 页数:19 大小:64.50KB
下载 相关 举报
函数式编程另类指南.doc_第1页
第1页 / 共19页
函数式编程另类指南.doc_第2页
第2页 / 共19页
函数式编程另类指南.doc_第3页
第3页 / 共19页
函数式编程另类指南.doc_第4页
第4页 / 共19页
函数式编程另类指南.doc_第5页
第5页 / 共19页
点击查看更多>>
资源描述

1、【转】函数式编程另类指南March 27th, 2007 : jackyz Goto comments Leave a comment 原文:Functional Programming For The Rest of Us原文作者:Vyacheslav Akhmechet翻译:lihaitao (电邮: lihaitao 在 )翻译原帖:函数式编程另类指南校对:刘凯清程序员拖沓成性,每天到了办公室后,泡咖啡,检查邮箱,阅读 RSS feed,到技术站点查阅最新的文章,在编程论坛的相关版面浏览公共讨论,并一次次地刷新以免漏掉一条信息。然后是午饭,回来后盯了 IDE 没几分钟,就再 次检查邮箱,

2、倒咖啡。最后在不知不觉中,结束了一天。不平凡的事是每隔一段时间会跳出一些很有挑战性的文章。如果没错,这些天你至少发现了一篇这类文章很难快速通读它们,于是就将之束之高阁,直到 突然你发现自己已经有了一个长长的链接列表和一个装满了 PDF 文件的目录,然后你梦想着到一个人迹罕至的森林里的小木屋苦读一年以期赶上,要是每天清晨你 沿着那里的林中小溪散步时会有人带来食物和带走垃圾就更好了。虽然我对你的列表一无所知,但我的列表却是一大堆关于函数式编程的文章。而这些基本上是最难阅读的了。它们用枯燥的学院派语言写成,即使“在华尔街 行业浸淫十年的专家(veterans) ”也不能理解函数式编程(也写作 FP)

3、都在探讨些什么。如果你去问花旗集团(Citi Group)或德意志银行( Deutsche Bank)的项目经理 1,为什么选择了 JMS 而不 Erlang,他们可能回答不能在产业级的应用中使用学院派语言。问题是,一些最为复杂的,有着最严格需求的系统却是用函数式编程元素写成。有些说法不能 让人信服。的确,关于函数式编程的文章和论文难于理解,但他们本来不必这么晦涩。这一知识隔阂的形成完全是历史原因。函数式编程的概念本身并不困难。这篇文章 可以作为“ 简易的函数式编程导引”。是一座从我们命令式(imperative)的思维模式到函数式编程的桥梁。去取杯咖啡回来继续读下去吧。可能你的同 事很快就会

4、开始取笑你对函数式编程发表的观点了。那么什么是函数式编程呢?它怎么产生?它可以被掌握吗(Is it edible)?如果它真如其倡导者所言,为什么没有在行业中得到更广泛的使用?为什么好像只有那些拿着博士学位的人才使用它?最要紧的是,为什么它就 TMD 这么难学?这些 closure, continuation, currying,惰性求值和无副作用等等究竟是些什么东西?没有大学参与的项目怎么使用它?为什么它看上去这么诡异于和我们命令式思想友好,圣洁和亲近 的一切的一切?我们将于不久扫清这些疑问。首先让我来解释形成实际生活和学界文章之间巨大隔阂的缘起,简单得像一次公园的散步。信步游园启动时间机器

5、,我们散步在两千多年以前的一个被遗忘了太久的春季明媚的日子,那是公元前 380 年。雅典城墙外的橄榄树树荫里,柏拉图和一个英俊的奴隶小男孩朝着学院走去。“天气真好”, “饮食不错 ”,然后话题开始转向哲思。“瞧那两个学生, ”为了使问题更容易理解,柏拉图仔细地挑选着用词, “你认为谁更高呢?”小男孩看着那两个人站着的水漕说, “他们差不多一样高”。柏拉图说:“你的差不多一样是什么意思?”。 “我在这里看他们是一样高的,不过我肯定如果走近些就会看出他们高度的差别。 ”柏拉图笑了,他正把这个孩子带到正确的方向。 “那么你是说,我们这个世界没有完全的等同了?”小男孩想了一会儿回答, “对,我不这样认

6、为,任何事物总有一些区别,即使我们看不到它。”这句话非常到位!“那么如果这世上没有完全的相等,你又是如何理解完全 相等这个概念的呢?”小男孩迷惑得说:“我不知道。 ”最初尝试着理解数学的本源(nature)时也会产生这种疑惑。柏拉图暗示这个世上的万物都只是一个对完美的近似。他还认识到我们即使没有接触到完美但依然可以理解这一概念。所以他得出结论,完美的数学形式只能 存在于另一个世界,我们通过和那个世界的某种联系在一定程度上知晓他们。很明显我们不能看到完美的圆,但我们可以理解什么是完美的圆并用数学公式将它表达 出来。那么,什么是数学?为什么宇宙可以用数学定理描述?数学可以描述宇宙中的所有现象吗?2

7、数学哲学是一个很复杂的课题。像大多数哲学学科一样它更倾向于提出问题而不是给出解答。这些意见中很多都循回绕转于一个事实,即数学实际上是一个谜 语:我们设置了一系列基本的不冲突的原理和一些可以施加于这些原理的操作规则,然后我们就能堆砌这些规则以形成更复杂的规则。数学家把这种方法叫做“形式 系统”或“演算” 。如果愿意,我们可以很快写出一个关于 Tetris(译者注:一种通常被称为俄罗斯方块的游戏)的形式系统。实际上,工作中的 Tetris 实现就是一个形式系统,只是被指定使用了个不常见的表现形式。人马座的那个生物文明也许不能理解我们的 Tetris 和圆的范式,因为可能他们唯一的感知输入是气味香橙

8、的橘子。他们也许永远不会发现 Tetris 范式,但很可能会有一个圆的范式。我们也可能将无法阅读它,因为我们的嗅觉没有那么复杂,可是一旦我们理解(pass)了那一范式的表示形式(通过这种传 感器和标准解码技术来理解这种语言) ,其底层的概念就可被任何智能文明所理解。有趣的是如果从来没有智能文明存在,Tetris 和圆的范式仍然严密合理,只是没有人注定将会发现他们。如果产生了一种智能文明,他就会发现一些形式系统来帮助描述宇宙的规律。但他还是不大可能发现 Tetris 因为宇宙中再没有和它相似的事物。在现实世界中这类无用的形式系统或迷题的例子数不胜数,Tetris 只是其中的一个典型。我们甚至不能

9、确定自然数是否是对客观世界的完整近似,至少我们可以简单的想像一个很大的数它不能用宇宙中任何东西描述,因为它以近乎 无穷。历史一瞥3再次启动时间机器,这一次的旅行近了很多,我们回到 1930 年代。大萧条正在蹂躏着那个或新或就的时代。空前的经济下挫影响着几乎所有阶层的家庭生活,只有少数人还能够保持着饥谨危机前的安逸。一些人就如此幸运地位列其中,我们关心的是普林斯顿大学的数学家们。采用了歌特式风格设计建造的新办公室给普林斯顿罩上天堂般的幸福光环,来自世界各地的逻辑学家被邀请到普林斯顿建设一个新的学部。虽然彼时的美国民 众已难能弄到一餐的面包,普林斯顿的条件则是可以在高高的穹顶下,精致雕凿的木质墙饰

10、边上整日的品茶讨论或款款慢步于楼外的林荫之中。阿隆左丘奇就是一个在这种近于奢侈的环境中生活着的数学家。他在普林斯顿获得本科学位后被邀留在研究生院继续攻读。阿隆左认为那里的建筑实属浮 华,所以他很少一边喝茶一边与人讨论数学,他也不喜欢到林中散步。阿隆左是一个孤独者:因为只有一个人时他才能以最高的效率工作。虽然如此,他仍与一些普 林斯顿人保持的定期的联系,其中包括阿兰图灵,约翰冯 诺依曼,和 kurt Grodel。这四个人都对形式系统很感兴趣,而不太留意现实世界,以便致力于解决抽象的数学难题。他们的难题有些共同之处:都是探索关于计算的问题。如果我们有 了无限计算能力的机器,哪些问题可以被解决?我

11、们可以使他们自动地得以解决吗?是否还是有些问题无法解决,为什么?不同设计的各种机器是否具有相同的计算 能力?通过和其它人的合作,阿隆左丘奇提出了一个被称为 lambda 演算的形式系统。这个系统本质上是一种虚拟的机器的编程语言,他的基础是一些以函数为参数和返回值的函数。函数用希腊字母 lambda 标识,这个形式系统因此得名4 。利用这一形式系统,阿隆左就可以对上述诸多问题推理并给出结论性的答案。独立于阿隆左,阿兰图灵也在进行着相似的工作,他提出了一个不同的形式系统(现在被称为图灵机) ,并使用这一系统独立得给出了和阿隆左相似的结论。后来被证明图灵机和 lambda 演算能力等同。我们的故事本

12、可以到此结束,我会就此歇笔,而你也将浏览到下一个页面,如果第二次世界大战没有在那时打响。整个世界笼罩在战争的火光和硝烟之中,美 国陆军和海军前所未有的大量使用炮弹,为了改进炮弹的精确度,部队组织了大批的科学家持续地计算微分方程以解出弹道发射轨迹。渐渐意识到这个任务用人力手 工完成太耗精力后,人们开始着手开发各种设备来攻克这个难关。第一个解出了弹道轨迹的机器是 IBM 制造的 Mark I 它重达 5 吨,有 75 万个组件,每秒可以完成三次操作。竞争当然没有就此结束,1949 年,EDVAC(Electronic Discrete Variable Automatic Computer,爱达瓦

13、克)被推出并获得了极大的成功。这是对冯诺依曼架构的第一个实践实例,实际上也是图灵机的第一个现实实现。那一年好运与阿隆左 丘奇无缘。直到 1950 年代将尽,一位 MIT 的教授 John McCarthy(也是普林斯顿毕业生)对阿隆左丘奇的工作产生了兴趣。1958 年,他公开了表处理语言 Lisp。Lisp 是对阿隆左丘奇的 lambda 演算的实现但同时它工作在冯诺依曼计算机上!很多计算机科学家认识到了 Lisp 的表达能力。1973 年,MIT 人工智能实验室的一组程序员开发了被称为 Lisp 机器的硬件阿隆左 lambda 演算的硬件实现!函数式编程函数式编程是对阿隆左丘奇理论的实践应用

14、。但也并非全部 lambda 演算都被应用到了实践中,因为 lambda 演算不是被设计为在物理局限下工作的。因此,象面向对象的编程一样,函数式编程是一系列理念,而不是严格的教条。现在有很多种函数式编程语言,他们中的大 多数以不同方式完成不同任务。在本文中我将就最广泛使用的源自函数式编程的思想作一解释,并将用 Java 语言举例。( 的确,你可以用 Java 写出函数式的 程序如果你有显著的受虐倾向) 。在下面的小节中,我将会把 Java 作为一种函数式语言,并对其稍加修改使它成为一种可用的函数式语言。现在开始吧。lambda 演算被设计用来探询关于计算的问题,所以函数式编程主要处理计算,并惊

15、人地用函数来完成这一过程。函数是函数式编程的基本单位,函数几乎被用于一切,包括 最简单的计算,甚至变量都由计算取代。在函数式编程中,变量只是表达式的别名(这样我们就不必把所有东西打在一行里) 。变量是不能更改的,所有变量只能被 赋值一次。用 Java 的术语来说,这意味着所有单一变量都被声明为 final(或 C+ 的 const) 。在函数式编程中没有非 final 的变量。final int i = 5;final int j = i + 3;因为函数式编程中所有变量都是 final 的,所以可以提出这样两个有趣的表述:没有必要总是写出关键字 final,没有必要把变量再称为变量。那么现在

16、我们对 Java 作出两个修改:在我们的函数式 Java 中所有变量默认都是 final 的,我们将变量(variable )称为符号(symbol ) 。就此你也许会质疑,用我们新创造的语言还能写出有些复杂度的程序吗?如果每个符号都是不可变更(non-mutalbe)的,那么就无法改变任何状 态!其实事实并非完全如此。在阿隆左研究其 lambda 演算时,他并不想将某个状态维护一段时间以期未来对其进行修改。他关注的是对数据的操作(也通常被称为”演算体 caculating stuff”) 。既然已被证明 lambda 演算与图灵机等价,它可以完成所有命令式编程语言能够完成的任务。那么,我们怎

17、么才能做到呢?答案是函数式程序能保存状态,只是它并非通过变量而是使用函数来保存状态。状态保存在函数的参数中,保存在堆栈上。如果你要保存某个状态一段时间并 时不时地对其进行一些修改,可以写个递归函数。举个例子,我们写个函数来翻转 Java 的字符串。记住,我们声明的每个变量默认都是 final 的。5String reverse(String arg) if(arg.length = 0) return arg;else return reverse(arg.substring(1, arg.length) + arg.substring(0,1);这个函数很慢因为它不断地调用自己6,它还也是个

18、嗜内存魔因为要持续分配对象。不过它的确是在用函数式风格。你可能会问,怎么有人会这样写程序?好的,我这就慢慢讲来:函数式编程的优点你可能会认为我根本无法对上面那个畸形的函数给出个合理的解释。我开始学习函数式编程时就是这么认为的。不过我是错了。有很好的理由使用这种风 格,当然其中一些属主观因素。例如,函数式程序被认为更容易阅读。因为每个街区的孩子都知道,是否容易理解在旁观者的眼中,所以我将略去这些主观方面的理 由。幸运的是,还有很多的客观理由。单元测试因为函数式编程的每一个符号都是 final 的,没有函数产生过副作用。因为从未在某个地方修改过值,也没有函数修改过在其作用域之外的量并被其他函数使用

19、(如类成员或全局变量) 。这意味着函数求值的结果只是其返回值,而惟一影响其返回值的就是函数的参数。这是单元测试者的梦中仙境(wet dream)。对被测试程序中的每个函数,你只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态。所有要做的就是传递代表了边际情况的参 数。如果程序中的每个函数都通过了单元测试,你就对这个软件的质量有了相当的自信。而命令式编程就不能这样乐观了,在 Java 或 C+ 中只检查函数的返回值还不够我们还必须验证这个函数可能修改了的外部状态。调试如果一个函数式程序不如你期望地运行,调试也是轻而易举。因为函数式程序的 bug 不依赖于执行前与其无关的代码路径,你遇

20、到的问题就总是可以再现。在命令式程序中,bug 时隐时现,因为在那里函数的功能依赖与其他函数的副作用,你可能会在和 bug 的产生无关的方向探寻很久,毫无收获。函数式程序就不是这样如果一个函数的结果是错误的,那么无论之前你还执行过什么,这个函数总是返回相同的错误结 果。一旦你将那个问题再现出来,寻其根源将毫不费力,甚至会让你开心。中断那个程序的执行然后检查堆栈,和命令式编程一样,栈里每一次函数调用的参数都 呈现在你眼前。但是在命令式程序中只有这些参数还不够,函数还依赖于成员变量,全局变量和类的状态(这反过来也依赖着这许多情况) 。函数式程序里函数只依 赖于它的参数,而那些信息就在你注视的目光下

21、!还有,在命令式程序里,只检查一个函数的返回值不能够让你确信这个函数已经正常工作了,你还要去查看那个函 数作用域外数十个对象的状态来确认。对函数式程序,你要做的所有事就是查看其返回值!沿着堆栈检查函数的参数和返回值,只要发现一个不尽合理的结果就进入那个函数然后一步步跟踪下去,重复这一个过程,直到它让你发现了 bug 的生成点。并行函数式程序无需任何修改即可并行执行。不用担心死锁和临界区,因为你从未用锁!函数式程序里没有任何数据被同一线程修改两次,更不用说两个不同的线程了。这意味着可以不假思索地简单增加线程而不会引发折磨着并行应用程序的传统问题。事实既然如此,为什么并不是所有人都在需要高度并行作

22、业的应用中采用函数式程序?嗯,他们正在这样做。爱立信公司设计了一种叫作 Erlang 的函数式语言并将它使用在需要极高抗错性和可扩展性的电信交换机上。还有很多人也发现了 Erlang 的优势并开始使用它。我们谈论的是电信通信控制系统,这与设计华尔街的典型系统相比对可靠性和可升级性要求高了得多。实际上,Erlang 系统并不可靠和易扩展,Java 才是。Erlang 系统只是坚如磐石。关于并行的故事还没有就此停止,即使你的程序本身就是单线程的,那么函数式程序的编译器仍然可以优化它使其运行于多个 CPU 上。请看下面这段代码:String s1 = somewhatLongOperation1()

23、;String s2 = somewhatLongOperation2();String s3 = concatenate(s1, s2);在函数编程语言中,编译器会分析代码,辨认出潜在耗时的创建字符串 s1 和 s2 的函数,然后并行地运行它们。这在命令式语言中是不可能的,因为在那 里,每个函数都有可能修改了函数作用域以外的状态并且其后续的函数又会依赖这些修改。在函数式语言里,自动分析函数并找出适合并行执行的候选函数简单的像 自动进行的函数内联化!在这个意义上,函数式风格的程序是“不会过时的技术(future proof)”(即使不喜欢用行业术语,但这回要破例一次)。硬件厂商已经无法让 CP

24、U 运行得更快了,于是他们增加了处理器核心的速度并因并行而获得了四 倍的速度提升。当然他们也顺便忘记提及我们的多花的钱只是用在了解决平行问题的软件上了。一小部分的命令式软件和 100% 的函数式软件都可以直接并行运行于这些机器上。代码热部署过去要在 Windows 上安装更新,重启计算机是难免的,而且还不只一次,即使是安装了一个新版的媒体播放器。Windows XP 大大改进了这一状态,但仍不理想(我今天工作时运行了 Windows Update,现在一个烦人的图标总是显示在托盘里除非我重启一次机器) 。Unix 系统一直以来以更好的模式运行,安装更新时只需停止系统相关的组件,而 不是整个操作

25、系统。即使如此,对一个大规模的服务器应用这还是不能令人满意的。电信系统必须 100%的时间运行,因为如果在系统更新时紧急拨号失效,就可 能造成生命的损失。华尔街的公司也没有理由必须在周末停止服务以安装更新。理想的情况是完全不停止系统任何组件来更新相关的代码。在命令式的世界里这是不可能的。考虑运行时上载一个 Java 类并重载一个新的定义,那么所有 这个类的实例都将不可用,因为它们被保存的状态丢失了。我们可以着手写些繁琐的版本控制代码来解决这个问题,然后将这个类的所有实例序列化,再销毁这些实 例,继而用这个类新的定义来重新创建这些实例,然后载入先前被序列化的数据并希望载入代码可以恰到地将这些数据

26、移植到新的实例。在此之上,每次更新都要重 新手动编写这些用来移植的代码,而且要相当谨慎地防止破坏对象间的相互关系。理论简单,但实践可不容易。对函数式的程序,所有的状态即传递给函数的参数都被保存在了堆栈上,这使的热部署轻而易举!实际上,所有我们需要做的就是对工作中的代码和新版本的 代码做一个差异比较,然后部署新代码。其他的工作将由一个语言工具自动完成!如果你认为这是个科幻故事,请再思考一下。多年来 Erlang 工程师一直更新着他们的运转着的系统,而无需中断它。机器辅助的推理和优化函数式语言的一个有趣的属性就是他们可以用数学方式推理。因为一种函数式语言只是一个形式系统的实现,所有在纸上完成的运算

27、都可以应用于用这种语言 书写的程序。编译器可以用数学理论将转换一段代码转换为等价的但却更高效的代码7。多年来关系数据库一直在进行着这类优化。没有理由不能把这一技术应 用到常规软件上。另外,还能使用这些技术来证明部分程序的正确,甚至可能创建工具来分析代码并为单元测试自动生成边界用例!对稳固的系统这种功能没有价值,但如果你 要设计心房脉冲产生器 (pace maker)或空中交通控制系统,这种工具就不可或缺。如果你编写的应用程序不是产业的核心任务,这类工具也是你强于竞争对手的杀手锏。高阶函数我记得自己在了解了上面列出的种种优点后曾想:“那都是非常好的特性,可是如果我不得不用天生就不健全的语言编程,

28、把一切变量声明为final 产生的代码将是垃圾一堆。 ” 这其实是误解。在如 Java 这般的命令式语言环境里,将所有变量声明为 final 没有用,但是在函数式语言里不是这样。函数式语言提供了不同的抽象工具它会使你忘记你曾经习惯于修改变量。高阶函数就是这样一种工具。函数式语言中的函数不同于 Java 或 C 中的函数,而是一个超集 它有着 Java 函数拥有的所有功能,但还有更多。创建函数的方式和 C 中相似:int add(int i, int j) return i + j;这意味着有些东西和同样的 C 代码有区别。现在扩展我们的 Java 编译器使其支持这种记法。当我们输入上述代码后编

29、译器会把它转换成下面的 Java 代码(别忘了,所有东西都是 final 的):class add_function_t int add(int i, int j) return i + j;add_function_t add = new add_function_t();这里的符号 add 并不是一个函数。这是一个有一个成员函数的很小的类。我们现在可以把 add 作为函数参数放入我们的代码中。还可以把它赋给另一个符号。我们在运行时创建的 add_function_t 的实例如果不再被使用就将会被垃圾回收掉。这些使得函数成为第一级的对象无异于整数或字符串。 (作为参数)操作函数的函数被称为高

30、阶函数。别让这个术语吓 着你,这和 Java 的 class 操作其它 class(把它们作为参数)没有什么区别。我们本可以把它们称为“高阶类” 但没有人注意到这个,因为 Java 背后没有一个强大的学术社区。那么怎样,何时应该使用高阶函数呢?我很高兴你这样问。如果你不曾考虑类的层次,就可能写出了一整团堆砌的代码块。当你发现其中一些行的代码重复出 现,就把他们提取成函数(幸运的是这些依然可以在学校里学到) 。如果你发现在那个函数里一些逻辑动作根据情况有变,就把他提取成高阶函数。糊涂了?下面是 一个来自我工作的实例:假如我的一些 Java 代码接受一条信息,用多种方式处理它然后转发到其他服务器。

31、class MessageHandler void handleMessage(Message msg) / msg.setClientCode(”ABCD_123);/ sendMessage(msg);/ 现在假设要更改这个系统,现在我们要把信息转发到两个服务器而不是一个。除了客户端的代码一切都像刚才一样第二个服务器希望这是另一种格式。怎么处理这种情况?我们可以检查信息的目的地并相应修改客户端代码的格式,如下:class MessageHandler void handleMessage(Message msg) / if(msg.getDestination().equals(”serv

32、er1) msg.setClientCode(”ABCD_123); else msg.setClientCode(”123_ABC”);/ sendMessage(msg);/ 然而这不是可扩展的方法,如果加入了更多的服务器,这个函数将线性增长,更新它会成为我的梦魇。面向对象的方法是把 MessageHandler 作为基类,在导出类中专业化客户代码操作:abstract class MessageHandler void handleMessage(Message msg) / msg.setClientCode(getClientCode();/ sendMessage(msg);abs

33、tract String getClientCode();/ class MessageHandlerOne extends MessageHandler String getClientCode() return “ABCD_123;class MessageHandlerTwo extends MessageHandler String getClientCode() return “123_ABCD”;现在就可以对每一个服务器实例化一个适合的类。添加服务器的操作变得容易维护了。但对于这么一个简单的修改仍然要添加大量的代码。为了支持不同的客户代码我们创建了两个新的类型!现在我们用高阶函数完

34、成同样的功能:class MessageHandler void handleMessage(Message msg, Function getClientCode) / Message msg1 = msg.setClientCode(getClientCode();/ sendMessage(msg1);/ String getClientCodeOne() return “ABCD_123;String getClientCodeTwo() return “123_ABCD”;MessageHandler handler = new MessageHandler();handler.ha

35、ndleMessage(someMsg, getClientCodeOne);没有创建新的类型和新的 class 层次,只是传入合适的函数作为参数,完成了面向对象方式同样的功能,同时还有一些额外的优点。没有使自己囿于类的 层次之中:可以在运行时传入函数并在任何时候以更高的粒度更少的代码修改他们。编译器高效地为我们生成了面向对象的“粘合”代码!除此之外,我们还获得了 所有函数式编程的其他好处。当然函数式语言提供的抽象不只这些,高阶函数只是一个开始:currying我认识的大多数人都读过“四人帮”的那本设计模式,任何自重的程序员都会告诉你那本书是语言中立的(agnostic),模式在软件工程中是通

36、用的,和使用的语言无关。这个说法颇为高贵,故而不幸的是,有违现实。函数式编程极具表达能力。在函数式语言中,语言既已达此高度,设计模式就不再是必需,最终你将设计模式彻底消除而以概念编程。适配器 (Adapter)模式就是这样的一个例子。(究竟适配器和 Facade 模式区别在哪里?可能有些人需要在这里再多费些篇章)。一旦语言有了叫作 currying 的技术,这一模式就可以被消除。currying.适配器模式最有名的是被应用在 Java 的“默认”抽象单元 class 上。在函数式编程里,模式被应用到函数。模式带有一个接口并将它转换成另一个对他人有用的接口。这有一个适配器模式的例子:int po

37、w(int i, int j);int square(int i)return pow(i, 2);上面的代码把一个整数幂运算接口转换成为了一个平方接口。在学术文章里,这个雕虫小技被叫作 currying(得名于逻辑学家 HaskellCurry,他曾将相关的数学理论形式化 )。因为在函数式编程中函数(反之如 class)被作为参数来回传递,currying 很频繁地被用来把函数调整为更适宜的接口。因为函数的接口是他的参数,使用 currying 可以减少参数的数目(如上例所示)。函数式语言内建了这一技术。不用手动地创建一个包装了原函数的函数,函数式语言可以为你代劳。同样地,扩展我们的语言,让

38、他支持这个技术:square = int pow(int i, 2);这将为我们自动创建出一个有一个参数的函数 square。他把第二个参数设置为 2 再调用函数 pow。这行代码会被编译为如下的 Java 代码:class square_function_t int square(int i) return pow(i, 2);square_function_t square = new square_function_t();正如你所见,通过简单地创建一个对原函数的包装,在函数式编程中,这就是 currying 快速简易创建包装的捷径。把精力集中在你的业务上,让编译器为你写出必要的代码!什

39、么时候使用 currying?这很简单,任何时候你想要使用适配器模式(包装)时。惰性求值惰性(或延迟)求值这一技术可能会变得非常有趣一旦我们采纳了函数式哲学。在讨论并行时已经见过下面的代码片断:String s1 = somewhatLongOperation1();String s2 = somewhatLongOperation2();String s3 = concatenate(s1, s2);在一个命令式语言中求值顺序是确定的,因为每个函数都有可能会变更或依赖于外部状态,所以就必须有序的执行这些函数:首先是somewhatLongOperation1,然后 somewhatLongO

40、peration2,最后 concatenate,在函数式语言里就不尽然了。前面提到只要确保没有函数修改或依赖于全局变量,somewhatLongOperation1 和 somewhatLongOperation2 可以被并行执行。但是如果我们不想同时运行这两个函数,还有必要保证有序的执行他们呢?答案是不。我们只在其他函数依赖于 s1 和 s2 时才需要执行这两个函 数。我们甚至在 concatenate 调用之前都不必执行他们可以把他们的求值延迟到 concatenate 函数内实际用到他们的位置。如果用一个带 有条件分支的函数替换concatenate 并且只用了两个参数中的一个,另一个

41、参数就永远没有必要被求值。在 Haskell 语言中,不确保一切都(完全)按顺序执行,因为 Haskell 只在必要时才会对其求值。惰性求值优点众多,但缺点也不少。我们会在这里讨论它的优点而在下一节中解释其缺点。优化惰性求值有客观的优化潜力。惰性编译器看函数式代码就像数学家面对的代数表达式可以注销一部分而完全不去运行它,重新调整代码段以求更高的 效率,甚至重整代码以降低出错,所有确定性优化(guaranteeing optimizations)不会破坏代码。这是严格用形式原语描述程序的巨大优势代码固守着数学定律并可以数学的方式进行推理。抽象控制结构惰性求值提供了更高一级的抽象,它使得不可能的事

42、情得以实现。例如,考虑实现如下的控制结构:unless(stock.isEuropean() sendToSEC(stock);我们希望只在祖先不是欧洲人时才执行 sendToSEC。如何实现 unless?如果没有惰性求值,我们需要某种形式的宏(macro)系统,但Haskell 这样的语言不需要它。把他实现为一个函数即可:void unless(boolean condition, List code) if(!condition)code;注意如果条件为真代码将不被执行。我们不能在一个严格(strict)的语言中再现这种求值,因为 unless 调用之前会先对参数进行求值。无穷(infi

43、nite)数据结构惰性求值允许定义无穷数据结构,对严格语言来说实现这个要复杂的多。考虑一个 Fibonacci 数列,显然我们无法在有限的时间内计算出或在有限的内存里保存一个无穷列表。在严格语言如 Java 中,只能定义一个能返回 Fibonacci 数列中特定成员的 Fibonacci 函数,在 Haskell中,我们对其进一步抽象并定义一个关于 Fibonacci 数的无穷列表,因为作为一个惰性的语言,只有列表中实际被用到的部分才会被求值。这使得可以抽象出很多问题并从一个更高的层次重新审视他们。 (例如,我们可以在一个无穷列表上使用表处理函数) 。缺点当然从来不存在免费的午餐。惰性求值有很

44、多的缺点,主要就在于,懒。有很多现实世界的问题需要严格求值。例如考虑下例:System.out.println(”Please enter your name: “);System.in.readLine();在惰性求值的语言里,不能保证第一行会在第二行之前执行!那么我们就不能进行输入输出操作,不能有意义地使用本地(native)接口(因为他们相 互依赖其副作用必须被有序的调用) ,从而与整个世界隔离。如果引入允许特定执行顺序的原语又将失去数学地推理代码的诸多好处(为此将葬送函数式编程与其相 关的所有优点) 。幸运的是,并非丧失了一切,数学家为此探索并开发出了许多技巧来保证在一定函数设置下(f

45、unction setting)代码以一特定的顺序执行。这样我们就赢得了两个世界。这些技术包括 continuation, monad 和 uniqueness typing(一致型别) 。我只会在本文中解释 continuation,把 monad 和 uniqueness typing 留到将来的文章中。有趣的是,除了确保函数求值顺序, continuation 在很多别的情况下也很有用。这点等一会儿就会提到。ContinuationsContinuations 对于程序设计的意义,就像达芬奇密码对人类历史的意义:即对人类最大秘密的惊人揭示。也许不是,但他在概念上的突破性至少和揭示了负数的

46、平方根意义等同。我们在学习函数时,只是学到了一半的事实,因为我们基于一个错误的假定:函数只能将结果返回到它的调用函数。在这个意思上 continuation 是广义的函数。函数不必要返回到其调用函数而可以返回到程序的任何地方。我们把”continuation” 作为参数传给一个函数,它指定了这个函数返回的位置。这个描述可能听起来更加复杂。看一下下面的代码:int i = add(5, 10);int j = square(i);函数 add 在其被调用的位置将结果 15 赋给了 i,接下来 i 的值被用来调用 square。注意所有的惰性求值编译器都不能调整这几行代码因为第二行依赖着第一行的成

47、功求值。下面用 continuation 风格又称 CPS (Continuation Programming Style) 来重写这段代码,这里函数 add 会将结果返回到 square 而不是原来的调用函数。int j = add(5, 10, square);这个例子中 add 有了另一个参数 一个 add 必须在它求值结束时用其返回值调用的函数。这里 square 是 add 的一个 continuation。这两种情况下, j 都将等于 255。这就是强制使惰性语言有序地求值两个表达式的第一个技巧。考虑下面这个(熟悉的)IO代码:System.out.println(”Please

48、enter your name: “);System.in.readLine();这两行不相依赖所以编译器会自由的重新调整他们的执行顺序。然而,如果我们用 CPS 来重写这段代码,就会有一个依赖,编译器会因此而强制对这两行代码有序执行!System.out.println(”Please enter your name: “, System.in.readLine);这里 println 需要用自己的返回结果作为参数去调用 readLine 并将 readLine 返回值作为自己的返回值。这样就能确保这两行被有序执行而且 readLine 一定被执行(因为整个计算期望最后的结果为结果) 。Ja

49、va 的 println 返回 void 但如果它返回的是一个抽象值(readLine 所期待的) ,我们就解决了这个问题!当然这样的链接函数调用很快就会使代码难以读懂,不过这个可以避免。比 如我们可以给语言添加些语法甜点 (syntactic sugar)就可以简单的按顺序输入表达式,然后由编译器自动为我们链接这些函数调用。这样就可以如愿地使用期望的求值顺序并保留一切函数式编程的好处 (包括数学地对我们程序进行推理的能力)!如果还是有迷惑,记住函数是只有一个成员的类的实例。重写上述代码使得 println 和 readLine 成为类的实例,这样就对一切都清楚了。如果我在此结束本节,那将仅仅涉及到 continuation 最浅显的应用。用 CPS 重写整个程序,那里所有的函数都增加一个额外的 continuation 参数并把函数结果传给它。也可以通过简单地把函数当作 continuation 函数(总是返回到调用者的函数)的特殊实例来将程序转为 CPS 风格

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

当前位置:首页 > 实用文档 > 简明教程

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报