├── FunctionalProgrammingForTheRestOfUs.cn.md ├── FunctionalProgrammingForTheRestOfUs.en.md └── README.md /FunctionalProgrammingForTheRestOfUs.cn.md: -------------------------------------------------------------------------------- 1 | # 傻瓜函数式编程 2 | 3 | 2006年6月19日,星期一 4 | 5 | ## 1.开篇 6 | 7 | 我们这些码农做事都是很拖拉的。每天例行报到后,先来点咖啡,看看邮件还有RSS订阅的文章。然后翻翻新闻还有那些技术网站上的更新,再过一遍编程论坛口水区里那些无聊的论战。最后从头把这些再看一次以免错过什么精彩的内容。然后就可以吃午饭了。饭饱过后,回来盯着IDE发一会呆,再看看邮箱,再去搞杯咖啡。光阴似箭,可以回家了…… 8 | 9 | (在被众人鄙视之前)我唯一想说的是,在这些拖拉的日子里总会时不时读到一些[不明觉厉](http://www.baike.com/wiki/%E4%B8%8D%E6%98%8E%E8%A7%89%E5%8E%89)的文章。如果没有打开不应该打开的网站,每隔几天你都可以看到至少一篇这样的东西。它们的共性:难懂,耗时,于是这些文章就慢慢的堆积成山了。很快你就会发现自己已经累积了一堆的收藏链接还有数不清的PDF文件,此时你只希望隐入一个杳无人烟的深山老林里什么也不做,用一年半载好好的消化这些私藏宝贝。当然,我是说最好每天还是能有人来给送吃的顺带帮忙打扫卫生倒垃圾,哇哈哈。 10 | 11 | 我不知道你都收藏了些什么,我的阅读清单里面相当大部分都是函数式编程相关的东东:基本上是最难啃的。这些文章充斥着无比枯燥的教科书语言,我想就连那些在华尔街浸淫10年以上的大牛都无法搞懂这些函数式编程(简称FP)文章到底在说什么。你可以去花旗集团或者德意志银行找个项目经理来问问[1](#f1):你们为什么要选JMS而不用Erlang?答案基本上是:我认为这个学术用的语言还无法胜任实际应用。可是,现有的一些系统不仅非常复杂还需要满足十分严苛的需求,它们就都是用函数式编程的方法来实现的。这,就说不过去了。 12 | 13 | 关于FP的文章确实比较难懂,但我不认为一定要搞得那么晦涩。有一些历史原因造成了这种知识断层,可是FP概念本身并不难理解。我希望这篇文章可以成为一个“FP入门指南”,帮助你从[指令式编程](http://zh.wikipedia.org/zh/%E6%8C%87%E4%BB%A4%E5%BC%8F%E7%B7%A8%E7%A8%8B)走向[函数式编程](http://zh.wikipedia.org/zh/%E5%87%BD%E6%95%B8%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80)。先来点咖啡,然后继续读下去。很快你对FP的理解就会让同事们刮目相看了。 14 | 15 | 什么是函数式编程(Functional Programming,FP)?它从何而来?可以吃吗?倘若它真的像那些鼓吹FP的人说的那么好,为什么实际应用中那么少见?为什么只有那些在读博士的家伙想要用它?而最重要的是,它母亲的怎么就那么难学?那些所谓的closure、continuation,currying,lazy evaluation还有no side effects都是什么东东(译者:本着保留专用术语的原则,此处及下文类似情形均不译)?如果没有那些大学教授的帮忙怎样把它应用到实际工程里去?为什么它和我们熟悉的万能而神圣的指令式编程那么的不一样? 16 | 17 | 我们很快就会解开这些谜团。刚才我说过实际工程和学术界之间的知识断层是有其历史原因的,那么就先让我来解释一下这个问题。答案,就在接下来的一次公园漫步中: 18 | 19 | ## 2.公园漫步 20 | 21 | 时间机器启动……我们来到公元前380年,也就是2000多年前的雅典城外。这是一个阳光明媚的久违的春天,[柏拉图](http://zh.wikipedia.org/zh/%E6%9F%8F%E6%8B%89%E5%9B%BE)和一个帅气的小男仆走在一片橄榄树荫下。他们正准备前往一个学院。天气很好,吃得很饱,渐渐的,两人的谈话转向了哲学。 22 | 23 | “你看那两个学生,哪一个更高一些?”,柏拉图小心的选择用字,以便让这个问题更好的引导眼前的这个小男孩。 24 | 25 | 小男仆望向水池旁边的两个男生,“他们差不多一样高。”。 26 | 27 | “ ‘差不多一样高’ 是什么意思?” , 柏拉图问。 28 | 29 | “嗯……从这里看来他们是一样高的,但是如果走近一点我肯定能看出差别来。” 30 | 31 | 柏拉图笑了。他知道这个小孩已经朝他引导的方向走了。“这么说来你的意思是世界上没有什么东西是完全相同的咯?” 32 | 33 | 思考了一会,小男孩回答:“是的。万物之间都至少有一丁点差别,哪怕我们无法分辨出来。” 34 | 35 | 说到点子上了!“那你说,如果世界上没有什么东西是完全相等的,你怎么理解‘完全相等’这个概念?” 36 | 37 | 小男仆看起来很困惑。“这我就不知道了。” 38 | 39 | 这是人类第一次试图了解数学的本质。柏拉图认为我们所在的世界中,万事万物都是完美模型的一个近似。他同时意识到虽然我们不能感受到完美的模型,但这丝毫不会阻止我们了解完美模型的概念。柏拉图进而得出结论:完美的数学模型只存在于另外一个世界,而因为某种原因我们却可以通过联系着这两个世界的一个纽带来认识这些模型。一个简单的例子就是完美的圆形。没有人见过这样的一个圆,但是我们知道怎样的圆是完美的圆,而且可以用公式把它描述出来。 40 | 41 | 如此说来,什么是数学呢?为什么可以用数学法则来描述我们的这个宇宙?我们所处的这个世界中万事万物都可以用数学来描述吗?[2](#f2) 42 | 43 | 数理哲学是一门很复杂的学科。它和其他多数哲学一样,更着重于提出问题而不是给出答案。数学就像拼图一样,很多结论都是这样推导出来的:先是确立一些互不冲突的基础原理,以及一些操作这些原理的规则,然后就可以把这些原理以及规则拼凑起来形成新的更加复杂的规则或是定理了。数学家把这种方法称为“形式系统”或是“演算”。如果你想做的话,可以用形式系统描述俄罗斯方块这个游戏。而事实上,俄罗斯方块这个游戏的实现,只要它正确运行,就是一个形式系统。只不过它以一种不常见的形式表现出来罢了。 44 | 45 | 如果[半人马阿尔法](http://zh.wikipedia.org/wiki/%E5%8D%8A%E4%BA%BA%E9%A9%AC%E5%BA%A7%CE%B1)上有文明存在的话,那里的生物可能无法解读我们的俄罗斯方块形式系统甚至是简单的圆形的形式系统,因为它们感知世界的唯一器官可能只有鼻子(译者:偶的妈你咋知道?)也许它们是无法得知俄罗斯方块的形式系统了,但是它们很有可能知道圆形。它们的圆形我们可能没法解读,因为我们的鼻子没有它们那么灵敏(译者:那狗可以么?)可是只要越过形式系统的表示方式(比如通过使用“超级鼻子”之类的工具来感知这些用味道表示的形式系统,然后使用标准的解码技术把它们翻译成人类能理解的语言),那么任何有足够智力的文明都可以理解这些形式系统的本质。 46 | 47 | 有意思的是,哪怕宇宙中完全不存在任何文明,类似俄罗斯方块还有圆形这样的形式系统依旧是成立的:只不过没有智慧生物去发现它们而已。这个时候如果忽然一个文明诞生了,那么这些具有智慧的生物就很有可能发现各种各样的形式系统,并且用它们发现的系统去描述各种宇宙法则。不过它们可能不会发现俄罗斯方块这样的形式系统,因为在它们的世界里没有俄罗斯方块这种东西嘛。有很多像俄罗斯方块这样的形式系统是与客观世界无关的,比如说自然数,很难说所有的自然数都与客观世界有关,随便举一个超级大的数,这个数可能就和世界上任何事物无关,因为这个世界可能不是无穷大的。 48 | 49 | ## 3.历史回眸[3](#f3) 50 | 51 | 再次启动时间机……这次到达的是20世纪30年代,离今天近了很多。无论[新](http://zh.wikipedia.org/wiki/%E6%96%B0%E5%A4%A7%E9%99%B8)[旧](http://zh.wikipedia.org/wiki/%E8%88%8A%E5%A4%A7%E9%99%B8)大陆,经济大萧条都造成了巨大的破坏。社会各阶层几乎每一个家庭都深受其害。只有极其少数的几个地方能让人们免于遭受穷困之苦。几乎没有人能够幸运的在这些避难所里度过危机,注意,我说的是几乎没有,还真的有这么些幸运儿,比如说当时普林斯顿大学的数学家们。 52 | 53 | 新建成的哥特式办公楼给普林斯顿大学带来一种天堂般的安全感。来自世界各地的逻辑学者应邀来到普林斯顿,他们将组建一个新的学部。正当大部分美国人还在为找不到一片面包做晚餐而发愁的时候,在普林斯顿却是这样一番景象:高高的天花板和木雕包覆的墙,每天品茶论道,漫步丛林。 54 | 55 | 一个名叫[阿隆佐·邱奇](http://zh.wikipedia.org/zh/%E9%98%BF%E9%9A%86%E4%BD%90%C2%B7%E9%82%B1%E5%A5%87)(Alonzo Church)的年轻数学家就过着这样优越的生活。阿隆佐本科毕业于普林斯顿后被留在研究院。他觉得这样的生活完全没有必要,于是他鲜少出现在那些数学茶会中也不喜欢到树林里散心。阿隆佐更喜欢独处:自己一个人的时候他的工作效率更高。尽管如此他还是和普林斯顿学者保持着联系,这些人当中有[艾伦·图灵](http://zh.wikipedia.org/zh/%E8%89%BE%E4%BC%A6%C2%B7%E5%9B%BE%E7%81%B5)、[约翰·冯·诺伊曼](http://zh.wikipedia.org/zh/%E7%BA%A6%E7%BF%B0%C2%B7%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC)、[库尔特·哥德尔](http://zh.wikipedia.org/zh-hant/%E5%BA%93%E5%B0%94%E7%89%B9%C2%B7%E5%93%A5%E5%BE%B7%E5%B0%94)。 56 | 57 | 这四个人都对形式系统感兴趣。相对于现实世界,他们更关心如何解决抽象的数学问题。而他们的问题都有这么一个共同点:都在尝试解答关于计算的问题。诸如:如果有一台拥有无穷计算能力的超级机器,可以用来解决什么问题?它可以自动的解决这些问题吗?是不是还是有些问题解决不了,如果有的话,是为什么?如果这样的机器采用不同的设计,它们的计算能力相同吗? 58 | 59 | 在与这些人的合作下,阿隆佐设计了一个名为[lambda演算](http://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97)的形式系统。这个系统实质上是为其中一个超级机器设计的编程语言。在这种语言里面,函数的参数是函数,返回值也是函数。这种函数用希腊字母lambda([λ](http://en.wikipedia.org/wiki/Lambda)),这种系统因此得名[4](#f4)。有了这种形式系统,阿隆佐终于可以分析前面的那些问题并且能够给出答案了。 60 | 61 | 除了阿隆佐·邱奇,艾伦·图灵也在进行类似的研究。他设计了一种完全不同的系统(后来被称为[图灵机](http://zh.wikipedia.org/zh/%E5%9B%BE%E7%81%B5%E6%9C%BA)),并用这种系统得出了和阿隆佐相似的答案。到了后来人们证明了图灵机和lambda演算的能力是一样的。 62 | 63 | 如果二战没有发生,这个故事到这里就应该结束了,我的这篇小文没什么好说的了,你们也可以去看看有什么其他好看的文章。可是二战还是爆发了,整个世界陷于火海之中。那时的美军空前的大量使用炮兵。为了提高轰炸的精度,军方聘请了大批数学家夜以继日的求解各种差分方程用于计算各种火炮发射数据表。后来他们发现单纯手工计算这些方程太耗时了,为了解决这个问题,各种各样的计算设备应运而生。IBM制造的Mark一号就是用来计算这些发射数据表的第一台机器。Mark一号重5吨,由75万个零部件构成,每一秒可以完成3次运算。 64 | 65 | 战后,人们为提高计算能力而做出的努力并没有停止。1949年第一台电子离散变量自动计算机诞生并取得了巨大的成功。它是[冯·诺伊曼设计架构](http://zh.wikipedia.org/zh/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84)的第一个实例,也是一台现实世界中实现的图灵机。相比他的这些同事,那个时候阿隆佐的运气就没那么好了。 66 | 67 | 到了50年代末,一个叫 John McCarthy 的MIT教授(他也是普林斯顿的硕士)对阿隆佐的成果产生了兴趣。1958年他发明了一种列表处理语言(Lisp),这种语言是一种阿隆佐lambda演算在现实世界的实现,而且它能在冯·诺伊曼计算机上运行!很多计算机科学家都认识到了Lisp强大的能力。1973年在MIT人工智能实验室的一些程序员研发出一种机器,并把它叫做Lisp机。于是阿隆佐的lambda演算也有自己的硬件实现了! 68 | 69 | ## 4.函数式编程 70 | 71 | 函数式编程是阿隆佐思想在现实世界中的实现。不过不是全部的lambda演算思想都可以运用到实际中,因lambda演算在设计的时候就不是为了在各种现实世界中的限制下工作的。所以,就像面向对象的编程思想一样,函数式编程只是一系列想法,而不是一套严苛的规定。有很多支持函数式编程的程序语言,它们之间的具体设计都不完全一样。在这里我将用Java写的例子介绍那些被广泛应用的函数式编程思想(没错,如果你是受虐狂你可以用Java写出函数式程序)。在下面的章节中我会在Java语言的基础上,做一些修改让它变成实际可用的函数式编程语言。那么现在就开始吧。 72 | 73 | Lambda演算在最初设计的时候就是为了研究计算相关的问题。所以函数式编程主要解决的也是计算问题,而出乎意料的是,是用函数来解决的!(译者:请理解原作者的苦心,我想他是希望加入一点调皮的风格以免读者在中途睡着或是转台……)。函数就是函数式编程中的基础元素,可以完成几乎所有的操作,哪怕最简单的计算,也是用函数完成的。我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式(这样我们就不用把所有的代码都写在同一行里了)。所以我们这里所说的“变量”是不能被修改的。所有的变量只能被赋一次初值。在Java中就意味着每一个变量都将被声明为`final`(如果你用C++,就是`const`)。在FP中,没有非`final`的变量。 74 | 75 | ```java 76 | final int i = 5; 77 | final int j = i + 3; 78 | ``` 79 | 80 | 既然FP中所有的变量都是`final`的,可以引出两个规定:一是变量前面就没有必要再加上`final`这个关键字了,二是变量就不能再叫做“变量”了……,于是现在开始对Java做两个改动:所有Java中声明的变量默认为`final`,而且我们把所谓的“变量”称为“符号”。 81 | 82 | 到现在可能会有人有疑问:这个新创造出来的语言可以用来写什么有用的复杂一些的程序吗?毕竟,如果每个符号的值都是不能修改的,那么我们就什么东西都不能改变了!别紧张,这样的说法不完全正确。阿隆佐在设计lambda演算的时候他并不想要保留状态的值以便稍后修改这些值。他更关心的是基于数据之上的操作(也就是更容易理解的“计算”)。而且,lambda演算和图灵机已经被证明了是具有同样能力的系统,因此指令式编程能做到的函数式编程也同样可以做到。那么,怎样才能做到呢? 83 | 84 | 事实上函数式程序是可以保存状态的,只不过它们用的不是变量,而是函数。状态保存在函数的参数中,也就是说在栈上。如果你需要保存一个状态一段时间并且时不时的修改它,那么你可以编写一个递归函数。举个例子,试着写一个函数,用来反转一个Java的字符串。记住咯,这个程序里的变量都是默认为`final`的[5](#f5)。 85 | 86 | ```java 87 | String reverse(String arg) { 88 | if(arg.length == 0) { 89 | return arg; 90 | } 91 | else { 92 | return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); 93 | } 94 | } 95 | ``` 96 | 97 | 这个方程运行起来会相对慢一些,因为它重复调用自己[6](#f6)。同时它也会大量的消耗内存,因为它会不断的分配创建内存对象。无论如何,它是用函数式编程思想写出来的。这时候可能有人要问了,为什么要用这种奇怪的方式编写程序呢?嘿,我正准备告诉你。 98 | 99 | ## 5.FP之优点 100 | 101 | 你大概已经在想:上面这种怪胎函数怎么也不合理嘛。在我刚开始学习FP的时候我也这样想的。不过后来我知道我是错的。使用这种方式编程有很多好处。其中一些是主观的。比如说有人认为函数式程序更容易理解。这个我就不说了,哪怕街上随便找个小孩都知道”容易理解“是多么主观的事情。幸运的是,客观方面的好处还有很多。 102 | 103 | ### 5.1.单元测试 104 | 105 | 因为FP中的每个符号都是final的,于是没有什么函数会有副作用。谁也不能在运行时修改任何东西,也没有函数可以修改在它作用域之外的值给其他函数继续使用(在指令式编程中可以用类成员或是全局变量做到)。这意味着决定函数执行结果的唯一因素就是它的返回值,而影响其返回值的唯一因素就是它的参数。 106 | 107 | 这正是单元测试工程师梦寐以求的啊。现在测试程序中的函数时只需要关注它的参数就可以了。完全不需要担心函数调用的顺序,也不用费心设置外部某些状态值。唯一需要做的就是传递一些可以代表边界条件的参数给这些函数。相对于指令式编程,如果FP程序中的每一个函数都能通过单元测试,那么我们对这个软件的质量必将信心百倍。反观Java或者C++,仅仅检查函数的返回值是不够的:代码可能修改外部状态值,因此我们还需要验证这些外部的状态值的正确性。在FP语言中呢,就完全不需要。 108 | 109 | ### 5.2.调试查错 110 | 111 | 如果一段FP程序没有按照预期设计那样运行,调试的工作几乎不费吹灰之力。这些错误是百分之一百可以重现的,因为FP程序中的错误不依赖于之前运行过的不相关的代码。而在一个指令式程序中,一个bug可能有时能重现而有些时候又不能。因为这些函数的运行依赖于某些外部状态, 而这些外部状态又需要由某些与这个bug完全不相关的代码通过某个特别的执行流程才能修改。在FP中这种情况完全不存在:如果一个函数的返回值出错了,它一直都会出错,无论你之前运行了什么代码。 112 | 113 | 一旦问题可以重现,解决它就变得非常简单,几乎就是一段愉悦的旅程。中断程序的运行,检查一下栈,就可以看到每一个函数调用时使用的每一个参数,这一点和指令式代码一样。不同的是指令式程序中这些数据还不足够,因为函数的运行还可能依赖于成员变量,全局变量,还有其他类的状态(而这些状态又依赖于类似的变量)。FP中的函数只依赖于传给它的参数,而这些参数就在眼前!还有,对指令式程序中函数返回值的检查并不能保证这个函数是正确运行的。还要逐一检查若干作用域以外的对象以确保这个函数没有对这些牵连的对象做出什么越轨的行为(译者:好吧,翻译到这里我自己已经有点激动了)。对于一个FP程序,你要做的仅仅是看一下函数的返回值。 114 | 115 | 把栈上的数据过一遍就可以得知有哪些参数传给了什么函数,这些函数又返回了什么值。当一个返回值看起来不对头的那一刻,跳进这个函数看看里面发生了什么。一直重复跟进下去就可以找到bug的源头! 116 | 117 | ### 5.3.并发执行 118 | 119 | 不需要任何改动,所有FP程序都是可以并发执行的。由于根本不需要采用锁机制,因此完全不需要担心死锁或是并发竞争的发生。在FP程序中没有哪个线程可以修改任何数据,更不用说多线程之间了。这使得我们可以轻松的添加线程,至于那些祸害并发程序的老问题,想都不用想! 120 | 121 | 既然是这样,为什么没有人在那些高度并行的那些应用程序中采用FP编程呢?事实上,这样的例子并不少见。爱立信开发了一种FP语言,名叫Erlang,并应用在他们的电信交换机上,而这些交换机不仅容错度高而且拓展性强。许多人看到了Erlang的这些优势也纷纷开始使用这一语言。在这里提到的电信交换控制系统远远要比华尔街上使用的系统具有更好的扩展性也更可靠。事实上,用Erlang搭建的系统并不具备可扩展性和可靠性,而Java可以提供这些特性。Erlang只是像岩石一样结实不容易出错而已。 122 | 123 | FP关于并行的优势不仅于此。就算某个FP程序本身只是单线程的,编译器也可以将其优化成可以在多CPU上运行的并发程序。以下面的程序为例: 124 | 125 | ```java 126 | String s1 = somewhatLongOperation1(); 127 | String s2 = somewhatLongOperation2(); 128 | String s3 = concatenate(s1, s2); 129 | ``` 130 | 131 | 如果是函数式程序,编译器就可以对代码进行分析,然后可能分析出生成字符串s1和s2的两个函数可能会比较耗时,进而安排它们并行运行。这在指令式编程中是无法做到的,因为每一个函数都有可能修改其外部状态,然后接下来的函数又可能依赖于这些状态的值。在函数式编程中,自动分析代码并找到适合并行执行的函数十分简单,和分析C的内联函数没什么两样。从这个角度来说用FP风格编写的程序是“永不过时”的(虽然我一般不喜欢说大话空话,不过这次就算个例外吧)。硬件厂商已经没办法让CPU运行得再快了。他们只能靠增加CPU核的数量然后用并行来提高运算的速度。这些厂商故意忽略一个事实:只有可以并行的软件才能让你花大价钱买来的这些硬件物有所值。指令式的软件中只有很小一部分能做到跨核运行,而所有的函数式软件都能实现这一目标,因为FP的程序从一开始就是可以并行运行的。 132 | 133 | ### 5.4.热部署 134 | 135 | 在Windows早期,如果要更新系统那可是要重启电脑的,而且还要重启很多次。哪怕只是安装一个新版本的播放器。到了XP的时代这种情况得到比较大的改善,尽管还是不理想(我工作的时候用的就是Windows,就在现在,我的系统托盘上就有个讨厌的图标,我不重启机子就不消失)。这一方面Unix好一些,曾经。只需要暂停一些相关的部件而不是整个操作系统,就可以安装更新了。虽然是要好一些了,对很多服务器应用来说这也还是不能接受的。电信系统要求的是100%的在线率,如果一个救急电话因为系统升级而无法拨通,成千上万的人就会因此丧命。同样的,华尔街的那些公司怎么也不能说要安装软件而在整个周末停止他们系统的服务。 136 | 137 | 最理想的情况是更新相关的代码而不用暂停系统的其他部件。对指令性程序来说是不可能的。想想看,试着在系统运行时卸载掉一个Java的类然后再载入这个类的新的实现,这样做的话系统中所有该类的实例都会立刻不能运行,因为该类的相关状态已经丢失了。这种情况下可能需绞尽脑汁设计复杂的版本控制代码,需要将所有这种类正在运行的实例[序列化](http://zh.wikipedia.org/wiki/%E5%BA%8F%E5%88%97%E5%8C%96),逐一销毁它们,然后创建新类的实例,将现有数据也序列化后装载到这些新的实例中,最后希望负责装载的程序可以正确的把这些数据移植到新实例中并正常的工作。这种事很麻烦,每次有新的改动都需要手工编写装载程序来完成更新,而且这些装载程序还要很小心,以免破坏了现有对象之间的联系。理论上是没问题,可是实际上完全行不通。 138 | 139 | FP的程序中所有状态就是传给函数的参数,而参数都是储存在栈上的。这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及新的代码获得一个diff,然后用这个diff更新现有的代码,新代码的热部署就完成了。其它的事情有FP的语言工具自动完成!如果还有人认为这只存在于科幻小说中,他需要再想想:多年来Erlang工程师已经使用这种技术对它们的系统进行升级而完全不用暂停运行了。 140 | 141 | ### 5.5.机器辅助证明及优化 142 | 143 | FP语言有一个特性很有意思,那就是它们是可以用数学方法来分析的。FP语言本身就是形式系统的实现,只要是能在纸上写出来的数学运算就可以用这种语言表述出来。于是只要能够用数学方法证明两段代码是一致的,编译器就可以把某段代码解析成在数学上等同的但效率又更高的另外一段代码[7](#f7)。 关系数据库已经用这种方法进行优化很多年了。没有理由在常规的软件行业就不能应用这种技术。 144 | 145 | 另外,还可以用这种方法来证明代码的正确性,甚至可以设计出能够自动分析代码并为单元测试自动生成边缘测试用例的工具出来!对于那些对缺陷零容忍的系统来说,这一功能简直就是无价之宝。例如心脏起搏器,例如飞行管控系统,这几乎就是必须满足的需求。哪怕你正在开发的程序不是为了完成什么重要核心任务,这些工具也可以帮助你写出更健壮的程序,直接甩竞争对手n条大街。 146 | 147 | ## 6. 高阶函数 148 | 149 | 我还记得在了解到FP以上的各种好处后想到:“这些优势都很吸引人,可是,如果必须非要用这种所有变量都是`final`的蹩脚语言,估计还是不怎么实用吧”。其实这样的想法是不对的。对于Java这样的指令式语言来说,如果所有的变量都是必须是`final`的,那么确实很束手束脚。然而对函数式语言来说,情况就不一样了。函数式语言提供了一种特别的抽象工具,这种工具将帮助使用者编写FP代码,让他们甚至都没想到要修改变量的值。高阶函数就是这种工具之一。 150 | 151 | FP语言中的函数有别于Java或是C。可以说这种函数是一个[全集](http://zh.wikipedia.org/wiki/%E5%85%A8%E9%9B%86):Java函数可以做到的它都能做,同时它还有更多的能力。首先,像在C里写程序那样创建一个函数: 152 | 153 | ```c 154 | int add(int i, int j) { 155 | return i + j; 156 | } 157 | ``` 158 | 159 | 看起来和C程序没什么区别,但是很快你就可以看出区别来。接下来我们扩展Java的编译器以便支持这种代码,也就是说,当我们写下以上的程序编译器会把它转化成下面的Java程序(别忘了,所有的变量都是final的): 160 | 161 | ```java 162 | class add_function_t { 163 | int add(int i, int j) { 164 | return i + j; 165 | } 166 | } 167 | 168 | add_function_t add = new add_function_t(); 169 | ``` 170 | 171 | 在这里,符号`add`并不是一个函数,它是只有一个函数作为其成员的简单的类。这样做有很多好处,可以在程序中把`add`当成参数传给其他的函数,也可以把`add`赋给另外一个符号,还可以在运行时创建`add_function_t`的实例然后在不再需要这些实例的时候由系统回收机制处理掉。这样做使得函数成为和`integer`或是`string`这样的[第一类对象](http://zh.wikipedia.org/zh/%E7%AC%AC%E4%B8%80%E9%A1%9E%E7%89%A9%E4%BB%B6)。对其他函数进行操作(比如说把这些函数当成参数)的函数,就是所谓的高阶函数。别让这个看似高深的名字吓倒你(译者:好死不死起个这个名字,初一看还准备搬出已经尘封的高数教材……),它和Java中操作其他类(也就是把一个类实例传给另外的类)的类没有什么区别。可以称这样的类为“高阶类”,但是没人会在意,因为Java圈里就没有什么很强的学术社团。(译者:这是高级黑吗?) 172 | 173 | 那么什么时候该用高阶函数,又怎样用呢?我很高兴有人问这个问题。设想一下,你写了一大堆程序而不考虑什么类结构设计,然后发现有一部分代码重复了几次,于是你就会把这部分代码独立出来作为一个函数以便多次调用(所幸学校里至少会教这个)。如果你发现这个函数里有一部分逻辑需要在不同的情况下实现不同的行为,那么你可以把这部分逻辑独立出来作为一个高阶函数。搞晕了?下面来看看我工作中的一个真实的例子。 174 | 175 | 假设有一段Java的客户端程序用来接收消息,用各种方式对消息做转换,然后发给一个服务器。 176 | 177 | ```java 178 | class MessageHandler { 179 | void handleMessage(Message msg) { 180 | // ... 181 | msg.setClientCode("ABCD_123"); 182 | // ... 183 | 184 | sendMessage(msg); 185 | } 186 | 187 | // ... 188 | } 189 | ``` 190 | 191 | 再进一步假设,整个系统改变了,现在需要发给两个服务器而不再是一个了。系统其他部分都不变,唯独客户端的代码需要改变:额外的那个服务器需要用另外一种格式发送消息。应该如何处理这种情况呢?我们可以先检查一下消息要发送到哪里,然后选择相应的格式把这个消息发出去: 192 | 193 | ```java 194 | class MessageHandler { 195 | void handleMessage(Message msg) { 196 | // ... 197 | if(msg.getDestination().equals("server1") { 198 | msg.setClientCode("ABCD_123"); 199 | } else { 200 | msg.setClientCode("123_ABC"); 201 | } 202 | // ... 203 | 204 | sendMessage(msg); 205 | } 206 | 207 | // ... 208 | } 209 | ``` 210 | 211 | 可是这样的实现是不具备扩展性的。如果将来需要增加更多的服务器,上面函数的大小将呈线性增长,使得维护这个函数最终变成一场噩梦。面向对象的编程方法告诉我们,可以把`MessageHandler`变成一个基类,然后将针对不同格式的消息编写相应的子类。 212 | 213 | ```java 214 | abstract class MessageHandler { 215 | void handleMessage(Message msg) { 216 | // ... 217 | msg.setClientCode(getClientCode()); 218 | // ... 219 | 220 | sendMessage(msg); 221 | } 222 | 223 | abstract String getClientCode(); 224 | 225 | // ... 226 | } 227 | 228 | class MessageHandlerOne extends MessageHandler { 229 | String getClientCode() { 230 | return "ABCD_123"; 231 | } 232 | } 233 | 234 | class MessageHandlerTwo extends MessageHandler { 235 | String getClientCode() { 236 | return "123_ABCD"; 237 | } 238 | } 239 | ``` 240 | 241 | 这样一来就可以为每一个接收消息的服务器生成一个相应的类对象,添加服务器就变得更加容易维护了。可是,这一个简单的改动引出了很多的代码。仅仅是为了支持不同的客户端行为代码,就要定义两种新的类型!现在来试试用我们刚才改造的语言来做同样的事情,注意,这种语言支持高阶函数: 242 | 243 | ```java 244 | class MessageHandler { 245 | void handleMessage(Message msg, Function getClientCode) { 246 | // ... 247 | Message msg1 = msg.setClientCode(getClientCode()); 248 | // ... 249 | 250 | sendMessage(msg1); 251 | } 252 | 253 | // ... 254 | } 255 | 256 | String getClientCodeOne() { 257 | return "ABCD_123"; 258 | } 259 | 260 | String getClientCodeTwo() { 261 | return "123_ABCD"; 262 | } 263 | 264 | MessageHandler handler = new MessageHandler(); 265 | handler.handleMessage(someMsg, getClientCodeOne); 266 | ``` 267 | 268 | 在上面的程序里,我们没有创建任何新的类型或是多层类的结构。仅仅是把相应的函数作为参数进行传递,就做到了和用面向对象编程一样的事情,而且还有额外的好处:一是不再受限于多层类的结构。这样做可以做运行时传递新的函数,可以在任何时候改变这些函数,而且这些改变不仅更加精准而且触碰的代码更少。这种情况下编译器其实就是在替我们编写面向对象的“粘合”代码(译者:又称胶水代码,粘接代码)!除此之外我们还可以享用FP编程的其他所有优势。函数式编程能提供的抽象服务还远不止于此。高阶函数只不过是个开始。 269 | 270 | ## 7.Currying 271 | 272 | 我遇见的大多数码农都读过“[四人帮](http://baike.baidu.com/view/66964.htm#2)”的那本《设计模式》。任何稍有自尊心的码农都会说这本书和语言无关,因此无论你用什么编程语言,当中提到的那些模式大体上适用于所有软件工程。听起来很厉害,然而事实却不是这样。 273 | 274 | 函数式语言的表达能力很强。用这种语言编程的时候基本不需要设计模式,因为这种语言层次已经足够高,使得使用者可以以概念编程,从而完全不需要设计模式了。以[适配器模式](http://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)为例(有人知道这个模式和[外观模式](http://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)有什么区别吗?怎么觉得有人为了出版合同的要求而硬生生凑页数?)(译者:您不愧是高级黑啊)。对于一个支持`currying`技术的语言来说,这个模式就是多余的。 275 | 276 | 在Java中最有名的适配器模式就是在其“默认”抽象单元中的应用:类。在函数式语言中这种模式其实就是函数。在这个模式中,一个接口被转换成另外一个接口,让不同的用户代码调用。接下来就有一个适配器模式的例子: 277 | 278 | ```java 279 | int pow(int i, int j); 280 | int square(int i) 281 | { 282 | return pow(i, 2); 283 | } 284 | ``` 285 | 286 | 上面的代码中`square`函数计算一个整数的平方,这个函数的接口被转换成计算一个整数的任意整数次幂。在学术圈里这种简单的技术就被叫做`currying`(因为逻辑学家[哈斯凯尔·加里](https://zh.wikipedia.org/wiki/%E5%93%88%E6%96%AF%E5%87%B1%E7%88%BE%C2%B7%E5%8A%A0%E9%87%8C)用其数学技巧将这种技术描述出来,于是就以他的名字来命名了)。在一个FP语言中函数(而不是类)被作为参数进行传递,currying常常用于转化一个函数的接口以便于其他代码调用。函数的接口就是它的参数,于是currying通常用于减少函数参数的数量(见前例)。 287 | 288 | 函数式语言生来就支持这一技术,于是没有必要为某个函数手工创建另外一个函数去包装并转换它的接口,这些函数式语言已经为你做好了。我们继续拓展Java来支持这一功能。 289 | 290 | ```java 291 | square = int pow(int i, 2); 292 | ``` 293 | 294 | 上面的语句实现了一个平方计算函数,它只需要一个参数。它会继而调用pow函数并且把第二个参数置为2。编译过后将生成以下Java代码: 295 | 296 | ```java 297 | class square_function_t { 298 | int square(int i) { 299 | return pow(i, 2); 300 | } 301 | } 302 | square_function_t square = new square_function_t(); 303 | ``` 304 | 305 | 从上面的例子可以看到,很简单的,函数pow的封装函数就创建出来了。在FP语言中currying就这么简单:一种可以快速且简单的实现函数封装的捷径。我们可以更专注于自己的设计,编译器则会为你编写正确的代码!什么时候使用currying呢?很简单,当你想要用适配器模式(或是封装函数)的时候,就是用currying的时候。 306 | 307 | ## 8.[惰性求值](http://zh.wikipedia.org/zh/%E6%83%B0%E6%80%A7%E6%B1%82%E5%80%BC) 308 | 309 | 惰性求值(或是延迟求值)是一种有趣的技术,而当我们投入函数式编程的怀抱后这种技术就有了得以实现的可能。前面介绍并发执行的时候已经提到过如下代码: 310 | 311 | ```java 312 | String s1 = somewhatLongOperation1(); 313 | String s2 = somewhatLongOperation2(); 314 | String s3 = concatenate(s1, s2); 315 | ``` 316 | 317 | 在指令式语言中以上代码执行的顺序是显而易见的。由于每个函数都有可能改动或者依赖于其外部的状态,因此必须顺序执行。先是计算`somewhatLongOperation1`,然后到`somewhatLongOperation2`,最后执行`concatenate`。函数式语言就不一样了。 318 | 319 | 在前面讨论过,`somewhatLongOperation1`和`somewhatLongOperation2`是可以并发执行的,因为函数式语言保证了一点:没有函数会影响或者依赖于全局状态。可是万一我们不想要这两个函数并发执行呢?这种情况下是不是也还是要顺序执行这些函数?答案是否定的。只有到了执行需要 s1、s2 作为参数的函数的时候,才真正需要执行这两个函数。于是在`concatenate`这个函数没有执行之前,都没有需要去执行这两个函数:这些函数的执行可以一直推迟到`concatenate()`中需要用到s1和s2的时候。假如把`concatenate`换成另外一个函数,这个函数中有条件判断语句而且实际上只会需要两个参数中的其中一个,那么就完全没有必要执行计算另外一个参数的函数了!Haskell语言就是一个支持惰性求值的例子。Haskell不能保证任何语句会顺序执行(甚至完全不会执行到),因为Haskell的代码只有在需要的时候才会被执行到。 320 | 321 | 除了这些优点,惰性求值也有缺点。这里介绍了它的优点,我们将在下一章节介绍这些缺点以及如何克服它们。 322 | 323 | ### 8.1.代码优化 324 | 325 | 惰性求值使得代码具备了巨大的优化潜能。支持惰性求值的编译器会像数学家看待代数表达式那样看待函数式程序:抵消相同项从而避免执行无谓的代码,安排代码执行顺序从而实现更高的执行效率甚至是减少错误。在此基础上优化是不会破坏代码正常运行的。严格使用形式系统的基本元素进行编程带来的最大的好处,是可以用数学方法分析处理代码,因为这样的程序是完全符合数学法则的。 326 | 327 | ### 8.2.抽象化控制结构 328 | 329 | 惰性求值技术提供了更高阶的抽象能力,这提供了实现程序设计独特的方法。比如说下面的控制结构: 330 | 331 | ```java 332 | unless(stock.isEuropean()) { 333 | sendToSEC(stock); 334 | } 335 | ``` 336 | 337 | 我们想要除了`stock`是`European`之外的情况下都执行函数`sendToSEC`。如何实现例子中的unless?如果没有惰性求值就需要求助于某种形式的宏(译者:用if不行么?),不过在像Haskell这样的语言中就不需要那么麻烦了。直接实现一个`unless`函数就可以! 338 | 339 | ```haskell 340 | void unless(boolean condition, List code) { 341 | if(!condition) 342 | code; 343 | } 344 | ``` 345 | 346 | 请注意,如果condition值为真,那就不会计算code。在其他严格语言(见[严格求值](http://zh.wikipedia.org/wiki/%E6%B1%82%E5%80%BC%E7%AD%96%E7%95%A5#.E4.B8.A5.E6.A0.BC.E6.B1.82.E5.80.BC_.28Strict_evaluation.29))中这种行为是做不到的,因为在进入unless这个函数之前,作为参数的code已经被计算过了。 347 | 348 | ### 8.3.无穷数据结构 349 | 350 | 惰性求值技术允许定义无穷数据结构,这要在严格语言中实现将非常复杂。例如一个储存Fibonacci数列数字的列表。很明显这样一个列表是无法在有限的时间内计算出这个无穷的数列并存储在内存中的。在像Java这样的严格语言中,可以定义一个Fibonacci函数,返回这个序列中的某个数。而在Haskell或是类似的语言中,可以把这个函数进一步抽象化并定义一个Fibonacci数列的无穷列表结构。由于语言本身支持惰性求值,这个列表中只有真正会被用到的数才会被计算出来。这让我们可以把很多问题抽象化,然后在更高的层面上解决它们(比如可以在一个列表处理函数中处理无穷多数据的列表)。 351 | 352 | ### 8.4.不足之处 353 | 354 | 俗话说天下没有免费的午餐™。惰性求值当然也有其缺点。其中最大的一个就是,嗯,惰性。现实世界中很多问题还是需要严格求值的。比如说下面的例子: 355 | 356 | ```java 357 | System.out.println("Please enter your name: "); 358 | System.in.readLine(); 359 | ``` 360 | 361 | 在惰性语言中没人能保证第一行会在第二行之前执行!这也就意味着我们不能处理IO,不能调用系统函数做任何有用的事情(这些函数需要按照顺序执行,因为它们依赖于外部状态),也就是说不能和外界交互了!如果在代码中引入支持顺序执行的代码原语,那么我们就失去了用数学方式分析处理代码的优势(而这也意味着失去了函数式编程的所有优势)。幸运的是我们还不算一无所有。数学家们研究了不同的方法用以保证代码按一定的顺序执行(in a functional setting?)。这一来我们就可以同时利用到函数式和指令式编程的优点了!这些方法有`continuations`,`monads`以及`uniqueness typing`。这篇文章仅仅介绍了`continuations`,以后再讨论`monads`和`uniqueness typing`。有意思的是,`coutinuations`除了可以强制代码以特定顺序执行之外还有其他很多用处,这些我们在后面也会提及。 362 | 363 | ## 9.Continuation 364 | 365 | `continuation`对于编程,就像是达芬奇密码对于人类历史一样:它揭开了人类有史以来最大的谜团。好吧,也许没有那么夸张,不过它们的影响至少和当年发现负数有平方根不相上下。 366 | 367 | 我们对函数的理解只有一半是正确的,因为这样的理解基于一个错误的假设:函数一定要把其返回值返回给调用者。按照这样的理解,`continuation`就是更加广义的函数。这里的函数不一定要把返回值传回给调用者,相反,它可以把返回值传给程序中的任意代码。`continuation`就是一种特别的参数,把这种参数传到函数中,函数就能够根据`continuation`将返回值传递到程序中的某段代码中。说得很高深,实际上没那么复杂。直接来看看下面的例子好了: 368 | 369 | ```java 370 | int i = add(5, 10); 371 | int j = square(i); 372 | ``` 373 | 374 | `add`这个函数将返回`15`然后这个值会赋给`i`,这也是`add`被调用的地方。接下来i的值又会被用于调用`square`。请注意支持惰性求值的编译器是不能打乱这段代码执行顺序的,因为第二个函数的执行依赖于第一个函数成功执行并返回结果。这段代码可以用 Continuation Pass Style(CPS)技术重写,这样一来add的返回值就不是传给其调用者,而是直接传到`square`里去了。 375 | 376 | ```java 377 | int j = add(5, 10, square); 378 | ``` 379 | 380 | 在上例中,add多了一个参数:一个函数,add必须在完成自己的计算后,调用这个函数并把结果传给它。这时`square`就是`add`的一个`continuation`。上面两段程序中j的值都是`225`。 381 | 382 | 这样,我们学习到了强制惰性语言顺序执行两个表达式的第一个技巧。再来看看下面IO程序(是不是有点眼熟?): 383 | 384 | ```java 385 | System.out.println("Please enter your name: "); 386 | System.in.readLine(); 387 | ``` 388 | 389 | 这两行代码彼此之间没有依赖关系,因此编译器可以随意的重新安排它们的执行顺序。可是只要用CPS重写它,编译器就必须顺序执行了,因为重写后的代码存在依赖关系了。 390 | 391 | ```java 392 | System.out.println("Please enter your name: ", System.in.readLine); 393 | ``` 394 | 395 | 这段新的代码中`println`需要结合其计算结果调用`readLine`,然后再返回`readLine`的返回值。这使得两个函数得以保证按顺序执行而且`readLine`总被执行(这是由于整个运算需要它的返回值作为最终结果)。Java的`println`是没有返回值的,但是如果它可以返回一个能被`readLine`接受的抽象值,问题就解决了!(译者:别忘了,这里作者一开始就在Java的基础上修改搭建自己的语言)当然,如果一直把函数按照这种方法串下去,代码很快就变得不可读了,可是没有人要求你一定要这样做。可以通过在语言中添加[语法糖](http://zh.wikipedia.org/wiki/%E8%AF%AD%E6%B3%95%E7%B3%96)的方式来解决这个问题,这样程序员只要按照顺序写代码,编译器负责自动把它们串起来就好了。于是就可以任意安排代码的执行顺序而不用担心会失去FP带来的好处了(包括可以用数学方法来分析我们的程序)!如果到这里还有人感到困惑,可以这样理解,函数只是有唯一成员的类的实例而已。试着重写上面两行程序,让`println`和`readLine`变成这种类的实例,所有问题就都搞清楚了。 396 | 397 | 到这里本章基本可以结束了,而我们仅仅了解到`continuation`的一点皮毛,对它的用途也知之甚少。我们可以用CPS完成整个程序,程序里所有的函数都有一个额外的`continuation`作为参数接受其他函数的返回值。还可以把任何程序转换为CPS的,需要做的只是把当中的函数看作是特殊的`continuation`(总是将返回值传给调用者的`continuation`)就可以了,简单到完全可以由工具自动完成(史上很多编译器就是这样做的)。 398 | 399 | 一旦将程序转为CPS的风格,有些事情就变得显而易见了:每一条指令都会有一些`continuation`,都会将它的计算结果传给某一个函数并调用它,在一个普通的程序中这个函数就是该指令被调用并且返回的地方。随便找个之前提到过的代码,比如说`add(5,10)`好了。如果`add`属于一个用CPS风格写出的程序,`add`的`continuation`很明显就是当它执行结束后要调用的那个函数。可是在一个非CPS的程序中,`add`的`continuation`又是什么呢?当然我们还是可以把这段程序转成CPS的,可是有必要这样做吗? 400 | 401 | 事实上没有必要。注意观察整个CPS转换过程,如果有人尝试要为CPS程序写编译器并且认真思考过就会发现:CPS的程序是不需要栈的!在这里完全没有函数需要做传统意义上的 “return” 操作,函数执行完后仅需要接着调用另外一个函数就可以了。于是就不需要在每次调用函数的时候把参数压栈再将它们从中取出,只要把这些参数存放在一片内存中然后使用跳转指令就解决问题了。也完全不需要保留原来的参数:因为这种程序里的函数都不返回,所以它们不会被用第二次! 402 | 403 | 简单点说呢,用CPS风格写出来的程序不需要栈,但是每次调用函数的时候都会要多加一个参数。非CPS风格的程序不需要额外的参数但又需要栈才能运行。栈里面存的是什么?仅仅是参数还有一个供函数运行结束后返回的程序指针而已。这个时候你是不是已经恍然大悟了?对啊,栈里面的数据实际上就是`continuation`的信息!栈上的程序返回指针实质上就是CPS程序中需要调用的下一个函数!想要知道`add(5, 10)`的`continuation`是什么?只要看它运行时栈的内容就可以了。 404 | 405 | 接下来就简单多了。`continuation`和栈上指示函数返回地址的指针其实是同一样东西,只是`continuation`是显式的传递该地址并且因此代码就不局限于只能返回到函数被调用的地方了。前面说过,`continuation`就是函数,而在我们特制的语言中函数就是类的实例,那么可以得知栈上指向函数返回地址的指针和`continuation`的参数是一样的,因为我们所谓的函数(就像类的一个实例)其实就是指针。这也意味着在程序运行的任何时候,你都可以得到当前的`continuation`(就是栈上的信息)。 406 | 407 | 好了,我们已经搞清楚当前的`continuation`是什么了。接下来要弄明白它的存在有什么意义。只要得到了当前的`continuation`并将它保存起来,就相当于保存了程序的当前状态:在时间轴上把它冻结起来了。这有点像操作系统进入休眠状态。`continuation`对象保存了足够的信息随时可以从指定的某个状态继续运行程序。在切换线程的时候操作系统也是这样做的。唯一的区别在于它保留了所有的控制权利。当请求某个`continuation`对象时(在Scheme语言中是通过调用`call-with-current-continuation`函数实现的)得到的是一个存有当前`continuation`的对象,也就是栈对象(在CPS中也就是下一个要执行的函数)。可以把这个对象保存做一个变量中(或者是存在磁盘上)。当以该`continuation`对象“重启”该程序时,程序的状态就会立即“转换”为该对象中保存的状态。这一点和切换回一个被暂停的线程或是从系统休眠中唤醒很相像,唯一不同的是`continuatoin`对象可以反复的这样使用。当系统唤醒后,休眠前保存的信息就会销毁,否则你也可以反复的从该点唤醒系统,就像乘时光机回到过去一样。有了`continuation`你就可以做到这一点! 408 | 409 | 那么`continuation`在什么情况下有用呢?有一些应用程序天生就没有状态,如果要在这样的系统中模拟出状态以简化工作的时候,就可以用到`continuation`。最合适的应用场合之一就是网页应用程序。微软的ASP.NET为了让程序员更轻松的编写应用程序,花了大量的精力去模拟各种状态。假如C#支持`continuation`的话,那么ASP.NET的复杂度将减半:因为只要把某一时刻的`continuation`保存起来,下次用户再次发起同样请求的时候,重新载入这个`continuation`即可。对于网络应用的程序员来说就再也没有中断了:轻轻松松程序就从下一行开始继续运行了!对于一些实际问题来说,`continuation`是一种非常有用的抽象工具。如今大量的传统胖客户端(见[瘦客户端](http://zh.wikipedia.org/wiki/%E7%98%A6%E5%AE%A2%E6%88%B7%E7%AB%AF))正纷纷走进网络,`continuation`在未来将扮演越来越重要的角色。 410 | 411 | ## 10.模式匹配 412 | 413 | 模式匹配并不是什么新功能。而事实上它和函数式编程也没有什么太大的关系。它之所以常常被认为是FP的一个特性,是因为在函数式语言已经支持模式匹配很长一段时间后的今天,指令式语言是还没有这个功能。 414 | 415 | 还是直接用例子来看看什么是模式匹配吧,这是一个用Java写的Fibonacci函数: 416 | 417 | ```java 418 | int fib(int n) { 419 | if(n == 0) return 1; 420 | if(n == 1) return 1; 421 | 422 | return fib(n - 2) + fib(n - 1); 423 | } 424 | ``` 425 | 426 | 再看看用我们基于Java修改过的新语言写出来的Fibonacci函数,这种新语言就支持模式匹配: 427 | 428 | ```java 429 | int fib(0) { 430 | return 1; 431 | } 432 | int fib(1) { 433 | return 1; 434 | } 435 | int fib(int n) { 436 | return fib(n - 2) + fib(n - 1); 437 | } 438 | ``` 439 | 440 | 区别在哪里呢?在于后者的编译器替我们实现了程序的分支。 441 | 442 | 这有什么了不起的?确实也没什么。只是有人注意到很多函数中有非常复杂的switch结构(对于函数式程序而言更是如此),于是想到如果能把这层结构也抽象化就更好了。然后就把这个复杂的函数拆分成若干新的函数,并在这些函数的某些参数中应用模式(这和[重载](http://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD)有点类似)。当这个函数被调用的时候,编译器会在运行时将调用者传入的参数与各个新函数的参数定义进行比较,找出合适的那个函数来执行。合适的函数往往是参数定义上最具体最接近传入参数的那个函数。在这个例子中,当n为1时,可以用函数int fib(int n),不过真正调用的是int fib(1)因为这个函数更具体更接近调用者的要求。 443 | 444 | 模式匹配一般来说要比这里举的例子更加复杂。比如说,高级模式匹配系统可以支持下面的操作: 445 | 446 | ```java 447 | int f(int n < 10) { ... } 448 | int f(int n) { ... } 449 | ``` 450 | 451 | 那么什么情况下模式匹配会有用呢?在需要处理一大堆程序分支的时候!每当需要实现复杂的嵌套if语句的时候,模式匹配可以帮助你用更少的代码更好的完成任务。我所知道的一个这样的函数是标准的WndProc函数,该函数是所有Win32应用程序必须具备的(尽管它经常会被抽象化)。模式匹配系统一般都可以像匹配简单数值一样匹配数据集合。举个例子,对于一个接受数组作为参数的函数,可以通过模式匹配数组中第一个数字为1并且第三个数字大于3的输入。 452 | 453 | 模式匹配的另外一个好处是每当需要添加或者修改程序分支时,再也不用面对那个庞大臃肿的函数了。只要添加(或者修改)相关的函数定义即可。有了模式匹配就不再需要四人帮的很多设计模式了。程序分支越多越复杂,模式匹配就越有用。而在习惯使用这一技术之后,你可能会怀疑没有它你一天都过不下去了。 454 | 455 | ## 11.Closure 456 | 457 | 目前为止关于函数式编程各种功能的讨论都只局限在“纯”函数式语言范围内:这些语言都是lambda演算的实现并且都没有那些和阿隆佐形式系统相冲突的特性。然而,很多函数式语言的特性哪怕是在lambda演算框架之外都是很有用的。确实,如果一个公理系统的实现可以用数学思维来看待程序,那么这个实现还是很有用的,但这样的实现却不一定可以付诸实践。很多现实中的语言都选择吸收函数式编程的一些元素,却又不完全受限于函数式教条的束缚。很多这样的语言(比如Common Lisp)都不要求所有的变量必须为`final`,可以修改他们的值。也不要求函数只能依赖于它们的参数,而是可以读写函数外部的状态。同时这些语言又包含了FP的特性,如高阶函数。与在lambda演算限制下将函数作为参数传递不同,在指令式语言中要做到同样的事情需要支持一个有趣的特性,人们常把它称为`lexical closure`。还是来看看例子。要注意的是,这个例子中变量不是final,而且函数也可以读写其外部的变量: 458 | 459 | ```java 460 | Function makePowerFn(int power) { 461 | int powerFn(int base) { 462 | return pow(base, power); 463 | } 464 | 465 | return powerFn; 466 | } 467 | 468 | Function square = makePowerFn(2); 469 | square(3); // returns 9 470 | ``` 471 | 472 | `makePowerFn`函数返回另一个函数,这个新的函数需要一个整数参数然后返回它的平方值。执行square(3)的时候具体发生了什么事呢?变量`power`并不在`powerFn`的域内,因为`makePowerFn`早就运行结束返回了,所以它的栈也已经不存在了。那么square又是怎么正常工作的呢?这个时候需要语言通过某种方式支持继续存储`power`的值,以便`square`后面继续使用。那么如果再定义一个函数,cube,用来计算立方,又应该怎么做呢?那么运行中的程序就必须存储两份`power`的值,提供给`makePowerFn`生成的两个函数分别使用。这种保存变量值的方法就叫做closure。closure不仅仅保存宿主函数的参数值,还可以用在下例的用法中: 473 | 474 | ```java 475 | Function makeIncrementer() { 476 | int n = 0; 477 | 478 | int increment() { 479 | return ++n; 480 | } 481 | } 482 | 483 | Function inc1 = makeIncrementer(); 484 | Function inc2 = makeIncrementer(); 485 | 486 | inc1(); // returns 1; 487 | inc1(); // returns 2; 488 | inc1(); // returns 3; 489 | inc2(); // returns 1; 490 | inc2(); // returns 2; 491 | inc2(); // returns 3; 492 | ``` 493 | 494 | 运行中的程序负责存储n的值,以便`incrementer`稍后可以访问它。与此同时,程序还会保存多份n的拷贝,虽然这些值应该在`makeIncrementer`返回后就消失,但在这个情况下却继续保留下来给每一个`incrementer`对象使用。这样的代码编译之后会是什么样子?closure幕后的真正工作机理又是什么?这次运气不错,我们有一个后台通行证,可以一窥究竟。 495 | 496 | 一点小常识往往可以帮大忙。乍一看这些本地变量已经不再受限于基本的域限制并拥有无限的生命周期了。于是可以得出一个很明显的结论:它们已经不是存在栈上,而是堆上了[8](#f8)。这么说来closure的实现和前面讨论过的函数差不多,只不过closure多了一个额外的引用指向其外部的变量而已: 497 | 498 | ```java 499 | class some_function_t { 500 | SymbolTable parentScope; 501 | 502 | // ... 503 | } 504 | ``` 505 | 506 | 当closure需要访问不在它本地域的变量时,就可以通过这个引用到更外一层的父域中寻找该变量。谜底揭开了!closure将函数编程与面向对象的方法结合了起来。下一次为了保存并传递某些状态而创建类的时候,想想closure。它能在运行时从相应的域中获得变量,从而可以把该变量当成“成员变量”来访问,也因为这样,就不再需要去创建一个成员变量了。 507 | 508 | ## 12.路在何方? 509 | 510 | 这篇文章仅仅涉及到函数式编程的一些皮毛。考虑到有时候星星之火可以燎原,所以如果它能给你一些帮助那就再好不过了。接下来我计划就[范畴论](http://zh.wikipedia.org/wiki/%E8%8C%83%E7%95%B4%E8%AE%BA)、monads、函数式编程数据结构、函数式语言中的[类型系统](http://zh.wikipedia.org/wiki/%E9%A1%9E%E5%9E%8B%E7%B3%BB%E7%B5%B1)、并行函数式编程、数据库的函数式编程以及更多的话题写些类似的文章。如果我可以写出(在我学习的同时)以上清单的一半,我的人生就完整了。于此同时,Google将是我们的良师益友。 511 | 512 | ## 13.欢迎联系 513 | 514 | 如果您有任何问题,评价或者建议,请发邮件到coffeemug@gmail.com(译者:如果翻译方面的问题/建议请发到yang.huang@ymail.com:))。期待您的回复。 515 | 516 | 注: 517 | 518 | 1当我在2005年求职时的的确确经常问别人这个问题。看着那些茫然的面孔实在是很好玩的事情。你们这些年薪30万美金的家伙,至少应该对自己可以利用的工具有个起码的理解嘛。[↩](#a1) 519 | 520 | 2这是个有争议的问题。物理学家和数学家不得不承认目前还无法确定宇宙万物是不是都遵从可以用数学方法描述的各种法则。[↩](#a2) 521 | 522 | 3我一直一来都很讨厌在历史课上罗列一堆枯燥无味的时间、人名、事件。对我来说历史就是关于那些改变世界的人们活生生的故事,是他们行为背后的个人动机,是那些他们用以影响芸芸众生的方法和工具。从这个角度来说,接下来的这堂历史课是不完整的,很遗憾。只有那些非常相关的人和事会被提及。[↩](#a3) 523 | 524 | 4在我学习函数式编程的时候,“lambda”这个术语搞得我很烦,因为我不知道它到底是什么意思。在这里lambda就是一个函数,在数学符号中用这个希腊字母只是因为它更容易写。所以以后在谈及函数式编程的时候只要你听到lambda,把它在脑中翻译为“函数”就可以了。[↩](#a4) 525 | 526 | 5有意思的是不论如何Java中的字符串总是不可修改的。讨论这种背叛Java的设计背后的原因会很有意思,可惜这样会让我们跑题的。[↩](#a5) 527 | 528 | 6大部分函数式语言的编译器都会尽量将迭代函数转换为对等的循环语句。这种做法叫做[尾调用优化](http://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8)。[↩](#a6) 529 | 530 | 7反之则不一定成立。尽管有时候可以证明两段代码是等价的,但不是在所有的情况下都可以得出这样的结论。[↩](#a7) 531 | 532 | 8实际上这样做并不比栈上存储要慢,因为在引入[垃圾回收机制](http://zh.wikipedia.org/wiki/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8))后,内存分配操作的时间代价仅为O(1)。[↩](#a8) -------------------------------------------------------------------------------- /FunctionalProgrammingForTheRestOfUs.en.md: -------------------------------------------------------------------------------- 1 | Functional Programming For The Rest of Us 2 | ============================================ 3 | Monday, June 19, 2006 4 | 5 | 6 | ### Introduction 7 | Programmers are procrastinators. Get in, get some coffee, check the mailbox, read the RSS feeds, read the news, check out latest articles on techie websites, browse through political discussions on the designated sections of the programming forums. Rinse and repeat to make sure nothing is missed. Go to lunch. Come back, stare at the IDE for a few minutes. Check the mailbox. Get some coffee. Before you know it, the day is over. 8 | 9 | The only thing, every once in a while challenging articles actually do pop up. If you're looking at the right places you'll find at least one of these every couple of days. These articles are hard to get through and take some time, so they start piling up. Before you know it, you have a list of links and a folder full of PDF files and you wish you had a year in a small hut in the middle of the forest with nobody around for miles so you could catch up. Would be nice if someone came in every morning while you're taking a walk down the river to bring some food and take out the garbage. 10 | 11 | I don't know about your list, but a large chunk of the articles in mine are about functional programming. These generally are the hardest to get through. Written in a dry academic language, even the "ten year Wall Street industry veterans" don't understand what functional programming (also referred to as FP) articles are all about. If you ask a project manager in Citi Group or in Deutsche Bank1 why they chose to use JMS instead of Erlang they'll say they can't use academic languages for industrial strength applications. The problem is, some of the most complex systems with the most rigid requirements are written using functional programming elements. Something doesn't add up. 12 | 13 | It's true that FP articles and papers are hard to understand, but they don't have to be. The reasons for the knowledge gap are purely historical. There is nothing inherently hard about FP concepts. Consider this article "an accessible guide to FP", a bridge from our imperative minds into the world of FP. Grab a coffee and keep on reading. With any luck your coworkers will start making fun of you for your FP comments in no time. 14 | 15 | So what is FP? How did it come about? Is it edible? If it's as useful as its advocates claim, why isn't it being used more often in the industry? Why is it that only people with PhDs tend to use it? Most importantly, why is it so damn hard to learn? What is all this closure, continuation, currying, lazy evaluation and no side effects business? How can it be used in projects that don't involve a university? Why does it seem to be so different from everything good, and holy, and dear to our imperative hearts? We'll clear this up very soon. Let's start with explaining the reasons for the huge gap between the real world and academic articles. The answer is as easy as taking a walk in the park. 16 | 17 | ### A Walk In The Park 18 | Fire up the time machine. Our walk in the park took place more than two thousand years ago, on a beautiful sunny day of a long forgotten spring in 380 B.C. Outside the city walls of Athens, under the pleasant shade of olive trees Plato was walking towards the Academy with a beautiful slave boy. The weather was lovely, the dinner was filling, and the conversation turned to philosophy. 19 | 20 | "Look at these two students", said Plato carefully picking words to make the question educational. "Who do you think is taller?" The slave boy looked towards the basin of water where two men were standing. "They're about the same height", he said. "What do you mean 'about the same'?", asked Plato. "Well, they look the same from here but I'm sure if I were to get closer I'd see that there is some difference." 21 | 22 | Plato smiled. He was leading the boy in the right direction. "So you would say that there is nothing perfectly equal in our world?" After some thinking the boy replied: "I don't think so. Everything is at least a little different, even if we can't see it." The point hit home! "Then if nothing is perfectly equal in this world, how do you think you understand the concept of 'perfect' equality?" The slave boy looked puzzled. "I don't know", he replied. 23 | 24 | So was born the first attempt to understand the nature of mathematics. Plato suggested that everything in our world is just an approximation of perfection. He also realized that we understand the concept of perfection even though we never encountered it. He came to conclusion that perfect mathematical forms must live in another world and that we somehow know about them by having a connection to that "alternative" universe. It's fairly clear that there is no perfect circle that we can observe. But we also understand what a perfect circle is and can describe it via equations. What is mathematics, then? Why is the universe described with mathematical laws? Can all of the phenomena of our universe be described by mathematics?2 25 | 26 | Philosophy of mathematics is a very complex subject. Like most philosophical disciplines it is far more adept at posing questions rather than providing answers. Much of the consensus revolves around the fact that mathematics is really a puzzle: we set up a set of basic non-conflicting principles and a set of rules on how to operate with these principles. We can then stack these rules together to come up with more complex rules. Mathematicians call this method a "formal system" or a "calculus". We can effectively write a formal system for Tetris if we wanted to. In fact, a working implementation of Tetris is a formal system, just specified using an unusual representation. 27 | 28 | A civilization of furry creatures on Alpha Centauri would not be able to read our formalisms of Tetris and circles because their only sensory input might be an organ that senses smells. They likely will never find out about the Tetris formalism, but they very well might have a formalism for circles. We probably wouldn't be able to read it because our sense of smell isn't that sophisticated, but once you get past the representation of the formalism (via various sensory instruments and standard code breaking techniques to understand the language), the concepts underneath are understandable to any intelligent civilization. 29 | 30 | Interestingly if no intelligent civilization ever existed in the universe the formalisms for Tetris and circles would still hold water, it's just that nobody would be around to find out about them. If an intelligent civilization popped up, it would likely discover some formalisms that help describe the laws of our universe. They also would be very unlikely to ever find out about Tetris because there is nothing in the universe that resembles it. Tetris is one of countless examples of a formal system, a puzzle, that has nothing to do with the real world. We can't even be sure that natural numbers have full resemblance to the real world, after all one can easily think of a number so big that it cannot describe anything in our universe since it might actually turn out to be finite. 31 | 32 | ### A Bit of History3 33 | 34 | Let's shift gears in our time machine. This time we'll travel a lot closer, to the 1930s. The Great Depression was ravaging the New and the Old worlds. Almost every family from every social class was affected by the tremendous economic downturn. Very few sanctuaries remained where people were safe from the perils of poverty. Few people were fortunate enough to be in these sanctuaries, but they did exist. Our interest lies in mathematicians in Princeton University. 35 | 36 | The new offices constructed in gothic style gave Princeton an aura of a safe haven. Logicians from all over the world were invited to Princeton to build out a new department. While most of America couldn't find a piece of bread for dinner, high ceilings, walls covered with elaborately carved wood, daily discussions by a cup of tea, and walks in the forest were some of the conditions in Princeton. 37 | The new offices constructed in gothic style gave Princeton an aura of a safe haven. Logicians from all over the world were invited to Princeton to build out a new department. While most of America couldn't find a piece of bread for dinner, high ceilings, walls covered with elaborately carved wood, daily discussions by a cup of tea, and walks in the forest were some of the conditions in Princeton. 38 | 39 | One mathematician living in such lavish lifestyle was a young man named Alonzo Church. Alonzo received a B.S. degree from Princeton and was persuaded to stay for graduate school. Alonzo felt the architecture was fancier than necessary. He rarely showed up to discuss mathematics with a cup of tea and he didn't enjoy the walks in the woods. Alonzo was a loner: he was most productive when working on his own. Nevertheless Alonzo had regular contacts with other Princeton inhabitants. Among them were Alan Turing, John von Neumann, and Kurt Gödel. 40 | 41 | The four men were interested in formal systems. They didn't pay much heed to the physical world, they were interested in dealing with abstract mathematical puzzles instead. Their puzzles had something in common: the men were working on answering questions about computation. If we had machines that had infinite computational power, what problems would we be able to solve? Could we solve them automatically? Could some problems remain unsolved and why? Would various machines with different designs be equal in power? 42 | 43 | In cooperation with other men Alonzo Church developed a formal system called lambda calculus. The system was essentially a programming language for one of these imaginary machines. It was based on functions that took other functions as parameters and returned functions as results. The function was identified by a Greek letter lambda, hence the system's name4. Using this formalism Alonzo was able to reason about many of the above questions and provide conclusive answers. 44 | 45 | Independently of Alonzo Church, Alan Turing was performing similar work. He developed a different formalism (now referred to as the Turing machine), and used it to independently come to similar conclusions as Alonzo. Later it was shown that Turing machines and lambda calculus were equivalent in power. 46 | 47 | This is where the story would stop, I'd wrap up the article, and you'd navigate to another page, if not for the beginning of World War II. The world was in flames. The U.S. Army and Navy used artillery more often than ever. In attempts to improve accuracy the Army employed a large group of mathematicians to continuously calculate differential equations required for solving ballistic firing tables. It was becoming obvious that the task was too great for being solved manually and various equipment was developed in order to overcome this problem. The first machine to solve ballistic tables was a Mark I built by IBM - it weighed five tons, had 750,000 parts and could do three operations per second. 48 | 49 | The race, of course, wasn't over. In 1949 an Electronic Discrete Variable Automatic Computer (EDVAC) was unveiled and had tremendous success. It was a first example of von Neumann's architecture and was effectively a real world implementation of a Turing machine. For the time being Alonzo Church was out of luck. 50 | 51 | In late 1950s an MIT professor John McCarthy (also a Princeton graduate) developed interest in Alonzo Church's work. In 1958 he unveiled a List Processing language (Lisp). Lisp was an implementation of Alonzo's lambda calculus that worked on von Neumann computers! Many computer scientists recognized the expressive power of Lisp. In 1973 a group of programmers at MIT's Artificial Intelligence Lab developed hardware they called a Lisp machine - effectively a native hardware implementation of Alonzo's lambda calculus! 52 | 53 | ### Functional Programming 54 | Functional programming is a practical implementation of Alonzo Church's ideas. Not all lambda calculus ideas transform to practice because lambda calculus was not designed to work under physical limitations. Therefore, like object oriented programming, functional programming is a set of ideas, not a set of strict guidelines. There are many functional programming languages, and most of them do many things very differently. In this article I will explain the most widely used ideas from functional languages using examples written in Java (yes, you could write functional programs in Java if you felt particularly masochistic). In the next couple of sections we'll take Java as is, and will make modifications to it to transform it into a useable functional language. Let's begin our quest. 55 | 56 | Lambda calculus was designed to investigate problems related to calculation. Functional programming, therefore, primarily deals with calculation, and, surprisingly, uses functions to do so. A function is a very basic unit in functional programming. Functions are used for almost everything, even the simplest of calculations. Even variables are replaced with functions. In functional programming variables are simply aliases for expressions (so we don't have to type everything on one line). They cannot be modified. All variables can only be assigned to once. In Java terms this means that every single variable is declared as final (or const if we're dealing with C++). There are no non-final variables in FP. 57 | 58 | ```java 59 | final int i = 5; 60 | final int j = i + 3; 61 | ``` 62 | 63 | Since every variable in FP is final two fairly interesting statements can be made. It does not make sense to always write the keyword final and it does not make sense to call variables, well... variables. We will now make two modifications to Java: every variable declared in our functional Java will be final by default, and we will refer to variables as symbols. 64 | 65 | By now you are probably wondering how you could possibly write anything reasonably complicated in our newly created language. If every symbol is non-mutable we cannot change the state of anything! This isn't strictly true. When Alonzo was working on lambda calculus he wasn't interested in maintaining state over periods of time in order to modify it later. He was interested in performing operations on data (also commonly referred to as "calculating stuff"). However, it was proved that lambda calculus is equivalent to a Turing machine. It can do all the same things an imperative programming language can. How, then, can we achieve the same results? 66 | 67 | It turns out that functional programs can keep state, except they don't use variables to do it. They use functions instead. The state is kept in function parameters, on the stack. If you want to keep state for a while and every now and then modify it, you write a recursive function. As an example, let's write a function that reverses a Java string. Remember, every variable we declare is final by default5. 68 | 69 | ```java 70 | String reverse(String arg) { 71 | if(arg.length == 0) { 72 | return arg; 73 | } 74 | else { 75 | return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); 76 | } 77 | } 78 | ``` 79 | 80 | This function is slow because it repeatedly calls itself6. It's a memory hog because it repeatedly allocates objects. But it's functional in style. You may be interested why someone would want to program in this manner. Well, I was just about to tell you. 81 | 82 | ### Benefits of FP 83 | You're probably thinking that there's no way I can rationalize the monstrosity of a function above. When I was learning functional programming I was thinking that too. I was wrong. There are very good arguments for using this style. Some of them are subjective. For example, people claim that functional programs are easier to understand. I will leave out these arguments because every kid on the block knows that ease of understanding is in the eye of the beholder. Fortunately for me, there are plenty of objective arguments. 84 | 85 | #### Unit Testing 86 | Since every symbol in FP is final, no function can ever cause side effects. You can never modify things in place, nor can one function modify a value outside of its scope for another function to use (like a class member or a global variable). That means that the only effect of evaluating a function is its return value and the only thing that affects the return value of a function is its arguments. 87 | 88 | This is a unit tester's wet dream. You can test every function in your program only worrying about its arguments. You don't have to worry about calling functions in the right order, or setting up external state properly. All you need to do is pass arguments that represent edge cases. If every function in your program passes unit tests you can be a lot more confident about quality of your software than if you were using an imperative language. In Java or C++ checking a return value of a function is not sufficient - it may modify external state that we would need to verify. Not so in a functional language. 89 | 90 | #### Debugging 91 | If a functional program doesn't behave the way you expect it to, debugging it is a breeze. You will always be able to reproduce your problem because a bug in a functional program doesn't depend on unrelated code paths that were executed before it. In an imperative program a bug resurfaces only some of the time. Because functions depend on external state produced by side effects from other functions you may have to go through a series of steps in no way related to the bug. In a functional program this isn't the case - if a return value of a function is wrong, it is always wrong, regardless of what code you execute before running the function. 92 | 93 | Once you reproduce the problem, getting to the bottom of it is trivial. It is almost pleasant. You break the execution of your program and examine the stack. Every argument in every function call in the stack is available for your inspection, just like in an imperative program. Except in an imperative program that's not enough because functions depend on member variables, global variables, and the state of other classes (which in turn depend on these very same things). A function in a functional program depends only on its arguments, and that information is right before your eyes! Furthermore, in an imperative program examining a return value of a function will not give you a good idea of whether the function behaves properly. You need to hunt down dozens of objects outside its scope to verify that it performed correct actions. In a functional program all you have to do is look at the return value! 94 | 95 | Walking through the stack you look at arguments passed to functions and their return values. The minute a return value doesn't make sense you step into the offending function and walk through it. You repeat this recursively until the process leads you to the source of the bug! 96 | 97 | #### Concurrency 98 | A functional program is ready for concurrency without any further modifications. You never have to worry about deadlocks and race conditions because you don't need to use locks! No piece of data in a functional program is modified twice by the same thread, let alone by two different threads. That means you can easily add threads without ever giving conventional problems that plague concurrency applications a second thought! 99 | 100 | If this is the case, why doesn't anybody use functional programs for highly concurrent applications? Well, it turns out that they do. Ericsson designed a functional language called Erlang for use in its highly tolerant and scalable telecommunication switches. Many others recognized the benefits provided by Erlang and started using it. We're talking about telecommunication and traffic control systems that are far more scalable and reliable than typical systems designed on Wall Street. Actually, Erlang systems are not scalable and reliable. Java systems are. Erlang systems are simply rock solid. 101 | 102 | The concurrency story doesn't stop here. If your application is inherently single threaded the compiler can still optimize functional programs to run on multiple CPUs. Take a look at the following code fragment: 103 | 104 | ```java 105 | String s1 = somewhatLongOperation1(); 106 | String s2 = somewhatLongOperation2(); 107 | String s3 = concatenate(s1, s2); 108 | ``` 109 | 110 | In a functional language the compiler could analyze the code, classify the functions that create strings s1 and s2 as potentially time consuming operations, and run them concurrently. This is impossible to do in an imperative language because each function may modify state outside of its scope and the function following it may depend on it. In functional languages automatic analysis of functions and finding good candidates for concurrent execution is as trivial as automatic inlining! In this sense functional style programs are "future proof" (as much as I hate buzzwords, I'll indulge this time). Hardware manufacturers can no longer make CPUs run any faster. Instead they increase the number of cores and attribute quadruple speed increases to concurrency. Of course they conveniently forget to mention that we get our money's worth only on software that deals with parallelizable problems. This is a very small fraction of imperative software but 100% of functional software because functional programs are all parallelizable out of the box. 111 | 112 | #### Hot Code Deployment 113 | In the old days of Windows in order to install updates it was necessary to restart the computer. Many times. After installing a newer version of a media players. With Windows XP the situation has improved significantly, yet it still isn't ideal (I ran Windows Update at work today and now an annoying system tray icon won't go away until I restart). Unix systems have had a better model for a while. In order to install an update you only need to stop relevant components, not the whole OS. While it is a better situation, for a large class of server applications it still isn't acceptable. Telecommunication systems need to be up 100% of the time because if dialing emergency is not available due to upgrades, lives may be lost. There is no reason Wall Street firms need to bring down their systems to install software updates over the weekend. 114 | 115 | An ideal situation is updating relevant parts of the code without stopping any part of the system at all. In an imperative world this isn't possible. Consider unloading a Java class at runtime and reloading a new definition. If we were to do that every instance of a class would become unusable because the state it holds would be lost. We would need to resort to writing tricky version control code. We'd need to serialize all running instances of the class, destroy them, create instances of the new class, try to load serialized data into them hoping the loading code properly migrates the data to work with the new instance. On top of that, every time we change something we'd have to write our migration code manually. And our migration code would have to take special care not to break relationships between objects. Nice in theory, but would never work well in practice. 116 | 117 | In a functional program all state is stored on the stack in the arguments passed to functions. This makes hot deployment significantly easier! In fact, all we'd really have to do is run a diff between the code in production and the new version, and deploy the new code. The rest could be done by language tools automatically! If you think this is science fiction, think again. Erlang engineers have been upgrading live systems without stopping them for years. 118 | 119 | #### Machine Assisted Proofs and Optimizations 120 | An interesting property of functional languages is that they can be reasoned about mathematically. Since a functional language is simply an implementation of a formal system, all mathematical operations that could be done on paper still apply to the programs written in that language. The compiler could, for example, convert pieces of code into equivalent but more efficient pieces with a mathematical proof that two pieces of code are equivalent7. Relational databases have been performing these optimizations for years. There is no reason the same techniques can't apply to regular software. 121 | 122 | Additionally, you can use these techniques to prove that parts of your program are correct. It is even possible to create tools that analyze code and generate edge cases for unit tests automatically! This functionality is invaluable for rock solid systems. If you are designing pace makers and air traffic control systems such tools are almost always a requirement. If you are writing an application outside of truly mission critical industries, these tools can give you a tremendous edge over your competitors. 123 | 124 | ### Higher Order Functions 125 | I remember learning about the benefits I outlined above and thinking "that's all very nice but it's useless if I have to program in a crippled language where everything is final." This was a misconception. Making all variables final is crippled in a context of an imperative language like Java but it isn't in a context of functional languages. Functional languages offer a different kind of abstraction tools that make you forget you've ever liked modifying variables. One such tool is capability to work with higher order functions. 126 | 127 | A function in such languages is different from a function in Java or C. It is a superset - it can do all the things a Java function can do, and more. We create a function in the same manner we do in C: 128 | 129 | ```c 130 | int add(int i, int j) { 131 | return i + j; 132 | } 133 | ``` 134 | 135 | This means something different from equivalent C code. Let's extend our Java compiler to support this notation. When we type something like this our compiler will convert it to the following Java code (don't forget, everything is final): 136 | 137 | ```java 138 | class add_function_t { 139 | int add(int i, int j) { 140 | return i + j; 141 | } 142 | } 143 | 144 | add_function_t add = new add_function_t(); 145 | ``` 146 | 147 | The symbol add isn't really a function. It is a small class with one function as its member. We can now pass add around in our code as an argument to other functions. We can assign it to another symbol. We can create instances of add_function_t at runtime and they will be garbage collected when we no longer need them. This makes functions first class objects no different from integers or strings. Functions that operate on other functions (accept them as arguments) are called higher order functions. Don't let this term intimidate you, it's no different from Java classes that operate on each other (we can pass class instances to other classes). We can call them "higher order classes" but nobody cares to because there is no strong academic community behind Java. 148 | 149 | How, and when, do you use higher order functions? Well, I'm glad you asked. You write your program as a big monolithic blob of code without worrying about class hierarchies. When you see that a particular piece of code is repeated, you break it out into a function (fortunately they still teach this in schools). If you see that a piece of logic within your function needs to behave differently in different situations, you break it out into a higher order function. Confused? Here's a real life example from my work. 150 | 151 | Suppose we have a piece of Java code that receives a message, transforms it in various ways, and forwards it to another server. 152 | 153 | ```java 154 | class MessageHandler { 155 | void handleMessage(Message msg) { 156 | // ... 157 | msg.setClientCode("ABCD_123"); 158 | // ... 159 | 160 | sendMessage(msg); 161 | } 162 | 163 | // ... 164 | } 165 | ``` 166 | 167 | Now imagine that our system has changed and we now route messages to two servers instead of one. Everything is handled in exactly the same way except the client code - the second server wants it in a different format. How do we handle this situation? We could check where the message is headed and format the client code differently, like this: 168 | 169 | ```java 170 | class MessageHandler { 171 | void handleMessage(Message msg) { 172 | // ... 173 | if(msg.getDestination().equals("server1") { 174 | msg.setClientCode("ABCD_123"); 175 | } else { 176 | msg.setClientCode("123_ABC"); 177 | } 178 | // ... 179 | 180 | sendMessage(msg); 181 | } 182 | 183 | // ... 184 | } 185 | ``` 186 | 187 | This approach, however, isn't scalable. If more servers are added our function will grow linearly and we'll have a nightmare updating it. An object oriented approach is to make MessageHandler a base class and specialize the client code operation in derived classes: 188 | 189 | ```java 190 | abstract class MessageHandler { 191 | void handleMessage(Message msg) { 192 | // ... 193 | msg.setClientCode(getClientCode()); 194 | // ... 195 | 196 | sendMessage(msg); 197 | } 198 | 199 | abstract String getClientCode(); 200 | 201 | // ... 202 | } 203 | 204 | class MessageHandlerOne extends MessageHandler { 205 | String getClientCode() { 206 | return "ABCD_123"; 207 | } 208 | } 209 | 210 | class MessageHandlerTwo extends MessageHandler { 211 | String getClientCode() { 212 | return "123_ABCD"; 213 | } 214 | } 215 | ``` 216 | 217 | We can now instantiate an appropriate class for each server. Adding servers becomes much more maintainable. That's a lot of code for such a simple modification though. We have to create two new types just to support different client codes! Now let's do the same thing in our language that supports higher order functions: 218 | 219 | ```java 220 | class MessageHandler { 221 | void handleMessage(Message msg, Function getClientCode) { 222 | // ... 223 | Message msg1 = msg.setClientCode(getClientCode()); 224 | // ... 225 | 226 | sendMessage(msg1); 227 | } 228 | 229 | // ... 230 | } 231 | 232 | String getClientCodeOne() { 233 | return "ABCD_123"; 234 | } 235 | 236 | String getClientCodeTwo() { 237 | return "123_ABCD"; 238 | } 239 | 240 | MessageHandler handler = new MessageHandler(); 241 | handler.handleMessage(someMsg, getClientCodeOne); 242 | ``` 243 | 244 | We've created no new types and no class hierarchy. We simply pass appropriate functions as a parameter. We've achieved the same thing as the object oriented counterpart with a number of advantages. We don't restrict ourselves to class hierarchies: we can pass new functions at runtime and change them at any time with a much higher degree of granularity with less code. Effectively the compiler has written object oriented "glue" code for us! In addition we get all the other benefits of FP. Of course the abstractions provided by functional languages don't stop here. Higher order functions are just the beginning. 245 | 246 | ### Currying 247 | Most people I've met have read the Design Patterns book by the Gang of Four. Any self respecting programmer will tell you that the book is language agnostic and the patterns apply to software engineering in general, regardless of which language you use. This is a noble claim. Unfortunately it is far removed from the truth. 248 | 249 | Functional languages are extremely expressive. In a functional language one does not need design patterns because the language is likely so high level, you end up programming in concepts that eliminate design patterns all together. Once such pattern is an Adapter pattern (how is it different from Facade again? Sounds like somebody needed to fill more pages to satisfy their contract). It is eliminated once a language supports a technique called currying. 250 | 251 | Adapter pattern is best known when applied to the "default" abstraction unit in Java - a class. In functional languages the pattern is applied to functions. The pattern takes an interface and transforms it to another interface someone else expects. Here's an example of an adapter pattern: 252 | 253 | ```java 254 | int pow(int i, int j); 255 | int square(int i) 256 | { 257 | return pow(i, 2); 258 | } 259 | ``` 260 | 261 | The code above adapts an interface of a function that raises an integer to an integer power to an interface of a function that squares an integer. In academic circles this trivial technique is called currying (after a logician Haskell Curry who performed mathematical acrobatics necessary to formalize it). Because in FP functions (as opposed to classes) are passed around as arguments, currying is used very often to adapt functions to an interface that someone else expects. Since the interface to functions is its arguments, currying is used to reduce the number of arguments (like in the example above). 262 | 263 | Functional languages come with this technique built in. You don't need to manually create a function that wraps the original, functional languages will do that for you. As usual, let's extend our language to support this technique. 264 | 265 | square = int pow(int i, 2); 266 | This will automatically create a function square for us with one argument. It will call pow function with the second argument set to 2. This will get compiled to the following Java code: 267 | 268 | ```java 269 | class square_function_t { 270 | int square(int i) { 271 | return pow(i, 2); 272 | } 273 | } 274 | square_function_t square = new square_function_t(); 275 | ``` 276 | 277 | As you can see, we've simply created a wrapper for the original function. In FP currying is just that - a shortcut to quickly and easily create wrappers. You concentrate on your task, and the compiler writes the appropriate code for you! When do you use currying? This should be easy. Any time you'd like to use an adapter pattern (a wrapper). 278 | 279 | #### Lazy Evaluation 280 | Lazy (or delayed) evaluation is an interesting technique that becomes possible once we adopt a functional philosophy. We've already seen the following piece of code when we were talking about concurrency: 281 | 282 | ```java 283 | String s1 = somewhatLongOperation1(); 284 | String s2 = somewhatLongOperation2(); 285 | String s3 = concatenate(s1, s2); 286 | ``` 287 | 288 | In an imperative language the order of evaluation would be clear. Because each function may affect or depend on an external state it would be necessary to execute them in order: first somewhatLongOperation1, then somewhatLongOperation2, followed by concatenate. Not so in functional languages. 289 | 290 | As we saw earlier somewhatLongOperation1 and somewhatLongOperation2 can be executed concurrently because we're guaranteed no function affects or depends on global state. But what if we don't want to run the two concurrently, do we need to run them in order? The answer is no. We only need to run these operations when another function depends on s1 and s2. We don't even have to run them before concatenate is called - we can delay their evaluation until they're required within concatenate. If we replace concatenate with a function that has a conditional and uses only one of its two parameters we may never evaluate one of the parameters at all! Haskell is an example of a delayed evaluation language. In Haskell you are not guaranteed that anything will be executed in order (or at all) because Haskell only executes code when it's required. 291 | 292 | Lazy evaluation has numerous advantages as well as disadvantages. We will discuss the advantages here and will explain how to counter the disadvantages in the next section. 293 | 294 | #### Optimization 295 | Lazy evaluation provides a tremendous potential for optimizations. A lazy compiler thinks of functional code exactly as mathematicians think of an algebra expression - it can cancel things out and completely prevent execution, rearrange pieces of code for higher efficiency, even arrange code in a way that reduces errors, all guaranteeing optimizations won't break the code. This is the biggest benefit of representing programs strictly using formal primitives - code adheres to mathematical laws and can be reasoned about mathematically. 296 | 297 | #### Abstracting Control Structures 298 | Lazy evaluation provides a higher order of abstraction that allows implementing things in a way that would otherwise be impossible. For example consider implementing the following control structure: 299 | 300 | ```java 301 | unless(stock.isEuropean()) { 302 | sendToSEC(stock); 303 | } 304 | ``` 305 | 306 | We want sendToSEC executed unless the stock is European. How can we implement unless? Without lazy evaluation we'd need some form of a macro system, but in a language like Haskell that's unnecessary. We can implement unless as a function! 307 | 308 | ```java 309 | void unless(boolean condition, List code) { 310 | if(!condition) 311 | code; 312 | } 313 | ``` 314 | 315 | Note that code is never evaluated if the condition is true. We cannot reproduce this behavior in a strict language because the arguments would be evaluated before unless is entered. 316 | #### Infinite Data Structures 317 | Lazy languages allow for definition of infinite data structures, something that's much more complicated in a strict language. For example, consider a list with Fibonacci numbers. We clearly can't compute and infinite list in a reasonable amount of time or store it in memory. In strict languages like Java we simply define a Fibonacci function that returns a particular member from the sequence. In a language like Haskell we can abstract it further and simply define an infinite list of Fibonacci numbers. Because the language is lazy, only the necessary parts of the list that are actually used by the program are ever evaluated. This allows for abstracting a lot of problems and looking at them from a higher level (for example, we can use list processing functions on an infinite list). 318 | 319 | #### Disadvantages 320 | Of course there ain't no such thing as a free lunch(tm). Lazy evaluation comes with a number of disadvantages. Mainly that it is, well, lazy. Many real world problems require strict evaluation. For example consider the following: 321 | 322 | ```java 323 | System.out.println("Please enter your name: "); 324 | System.in.readLine(); 325 | ``` 326 | 327 | In a lazy language you have no guarantee that the first line will be executed before the second! This means we can't do IO, can't use native functions in any meaningful way (because they need to be called in order since they depend on side effects), and can't interact with the outside world! If we were to introduce primitives that allow ordered code execution we'd lose the benefits of reasoning about our code mathematically (which would take all of the benefits of functional programming with it). Fortunately not all is lost. Mathematicians got to work and developed a number of tricks to ensure code gets executed in particular order in a functional setting. We get the best of both worlds! These techniques include continuations, monads, and uniqueness typing. In this article we'll only deal with continuations. We'll leave monads and uniqueness typing for another time. Interestingly, continuations are useful for many things other than enforcing a particular order of evaluation. We'll talk about that as well. 328 | 329 | ### Continuations 330 | Continuations to programming are what Da Vinci Code is to human history: an amazing revelation of the greatest cover-up known to man. Well, may be not, but they're certainly revealing of deceit in the same sense as square roots of negative numbers. 331 | 332 | When we learned about functions we only learned half truths based on a faulty assumption that functions must return their value to the original caller. In this sense continuations are a generalization of functions. A function must not necessarily return to its caller and may return to any part of the program. A "continuation" is a parameter we may choose to pass to our function that specifies where the function should return. The description may be more complicated than it sounds. Take a look at the following code: 333 | 334 | ```java 335 | int i = add(5, 10); 336 | int j = square(i); 337 | ``` 338 | 339 | The function add returns 15 to be assigned to i, the place where add was originally called. After that the value of i is used to call square. Note that a lazy compiler can't rearrange these lines of code because the second line depends on successful evaluation of the first. We can rewrite this code block using Continuation Passing Style or CPS, where the function add doesn't return to the original caller but instead returns its result to square. 340 | 341 | ```java 342 | int j = add(5, 10, square); 343 | ``` 344 | 345 | In this case add gets another parameter - a function that add must call with its result upon completion. In this case square is a continuation of add. In both cases j will equal 225. 346 | 347 | Here lays the first trick to force a lazy language to evaluate two expressions in order. Consider the following (familiar) IO code: 348 | 349 | ```java 350 | System.out.println("Please enter your name: "); 351 | System.in.readLine(); 352 | ``` 353 | 354 | The two lines don't depend on each other and the compiler is free to rearrange them as it wishes. However, if we rewrite this code in CPS, there will be a dependency and the compiler will be forced to evaluate the two lines in order! 355 | 356 | ```java 357 | System.out.println("Please enter your name: ", System.in.readLine); 358 | ``` 359 | 360 | 361 | In this case `println `needs to call `readLine `with its result and return the result of `readLine`. This allows us to ensure that the two lines are executed in order and that `readLine `is evaluated at all (because the whole computation expects the last value as a result). In case of Java `println `returns void but if it were to return an abstract value (that `readLine `would accept), we'd solve our problem! Of course chaining function calls like that will quickly become unreadable, but it isn't necessary. We could add syntactic sugar to the language that will allow us to simply type expressions in order, and the compiler would chain the calls for us automatically. We can now evaluate expressions in any order we wish without losing any of the benefits of FP (including the ability to reason about our programs mathematically)! If this is still confusing, remember that a function is just an instance of a class with one member. Rewrite above two lines so that `println `and `readLine `are instances of classes and everything will become clear. 362 | 363 | I would now wrap up this section, except that we've only scratched the surface of continuations and their usefulness. We can write entire programs in CPS, where every function takes an extra continuation argument and passes the result to it. We can also convert any program to CPS simply by treating functions as special cases of continuations (functions that always return to their caller). This conversion is trivial to do automatically (in fact, many compilers do just that). 364 | 365 | Once we convert a program to CPS it becomes clear that every instruction has some continuation, a function it will call with the result, which in a regular program would be a place it must return to. Let's pick any instruction from above code, say add(5, 10). In a program written in CPS style it's clear what add's continuation is - it's a function that add calls once it's done. But what is it in a non-CPS program? We could, of course, convert the program to CPS, but do we have to? 366 | 367 | It turns out that we don't. Look carefully at our CPS conversion. If you try to write a compiler for it and think about it long enough you'll realize that the CPS version needs no stack! No function ever "returns" in the traditional sense, it just calls another function with the result instead. We don't need to push function arguments on the stack with every call and then pop them back, we can simply store them in some block of memory and use a jump instruction instead. We'll never need the original arguments - they'll never be used again since no function ever returns! 368 | 369 | So, programs written in CPS style have no stack but have an extra argument with a function to call. Programs not written in CPS style have no argument with a function to call, but have the stack instead. What does the stack contain? Simply the arguments, and a pointer to memory where the function should return. Do you see a light bulb? The stack simply contains continuation information! The pointer to the return instruction in the stack is essentially the same thing as the function to call in CPS programs! If you wanted to find out what continuation for add(5, 10) is, you'd simply have to examine the stack at the point of its execution! 370 | 371 | So that was easy. A continuation and a pointer to the return instruction in the stack are really the same thing, only a continuation is passed explicitly, so that it doesn't need to be the same place where the function was called from. If you remember that a continuation is a function, and a function in our language is compiled to an instance of a class, you'll realize that a pointer to the return instruction in the stack and the continuation argument are really the same thing, since our function (just like an instance of a class) is simply a pointer. This means that at any given point in time in your program you can ask for a current continuation (which is simply the information on the stack). 372 | 373 | Ok, so we know what a current continuation is. What does it mean? When we get a current continuation and store it somewhere, we end up storing the current state of our program - freezing it in time. This is similar to an OS putting itself into hibernation. A continuation object contains the information necessary to restart the program from the point where the continuation object was acquired. An operating system does this to your program all the time when it context switches between the threads. The only difference is that it keeps all the control. If you ask for a continuation object (in Scheme this is done by calling call-with-current-continuation function) you'll get an object that contains the current continuation - the stack (or in a CPS case the function to call next). You can store this object in a variable (or alternatively, on disk). When you choose to "restart" your program with this continuation object you will "transform" to the state of the program when you grabbed the continuation object. It's the same thing as switching back to a suspended thread or waking up an OS from hibernation, except you can do it again and again. When an OS wakes up, the hibernation information is destroyed. If it wasn't, you'd be able to wake up from the same point over and over again, almost like going back in time. You have that control with continuations! 374 | 375 | In what situations are continuations useful? Usually when you're trying to simulate state in an application of inherently stateless nature to ease your life. A great application of continuations are web applications. Microsoft's ASP.NET goes to tremendous lengths to try and simulate state so that you can write your application with less hassle. If C# supported continuations half of ASP.NET's complexity would disappear - you'd simply store a continuation and restart it when a user makes the web request again. To a programmer of the web application there would be no interruption - the program would simply start from the next line! Continuations are an incredibly useful abstraction for some problems. Considering that many of the traditional fat clients are moving to the web, continuations will become more and more important in the future. 376 | 377 | ### Pattern Matching 378 | Pattern matching is not a new or innovative feature. In fact, it has little to do with functional programming. The only reason why it's usually attributed to FP is that functional languages have had pattern matching for some time, while modern imperative languages still don't. 379 | 380 | Let's dive into pattern matching with an example. Here's a Fibonacci function in Java: 381 | 382 | ```java 383 | int fib(int n) { 384 | if(n == 0) return 1; 385 | if(n == 1) return 1; 386 | 387 | return fib(n - 2) + fib(n - 1); 388 | } 389 | ``` 390 | 391 | And here's an example of a Fibonacci function in our Java-derived language that supports pattern matching: 392 | 393 | ```java 394 | int fib(0) { 395 | return 1; 396 | } 397 | int fib(1) { 398 | return 1; 399 | } 400 | int fib(int n) { 401 | return fib(n - 2) + fib(n - 1); 402 | } 403 | ``` 404 | 405 | What's the difference? The compiler implements branching for us. 406 | 407 | What's the big deal? There isn't any. Someone noticed that a large number of functions contain very complicated switch statements (this is particularly true about functional programs) and decided that it's a good idea to abstract that away. We split the function definition into multiple ones, and put patterns in place of some arguments (sort of like overloading). When the function is called, the compiler compares the arguments with the definitions at runtime, and picks the correct one. This is usually done by picking the most specific definition available. For example, int fib(int n) can be called with n equal to 1, but it isn't because int fib(1) is more specific. 408 | 409 | Pattern matching is usually more complex than our example reveals. For example, an advanced pattern matching system will allow us to do the following: 410 | 411 | ```java 412 | int f(int n < 10) { ... } 413 | int f(int n) { ... } 414 | ``` 415 | 416 | When is pattern matching useful? In a surprisingly large number of cases! Every time you have a complex structure of nested ifs, pattern matching can do a better job with less code on your part. A good function that comes to mind is a standard WndProc function that all Win32 applications must provide (even though it's often abstracted away). Usually a pattern matching system can examine collections as well as simple values. For example, if you pass an array to your function you could pick out all arrays in which the first element is equal to 1 and the third element is greater than 3. 417 | 418 | Another benefit of pattern matching is that if you need to add or modify conditions, you don't have to go into one huge function. You simply add (or modify) appropriate definitions. This eliminates the need for a whole range of design patterns from the GoF book. The more complex your conditions are, the more pattern matching will help you. Once you're used to it, you start wondering how you ever got through your day without it. 419 | 420 | ### Closures 421 | So far we've discussed features in the context of "pure" functional languages - languages that are implementations of lambda calculus and don't include features that conflict with Church's formalism. However, many of the features of functional languages are useful outside of lambda calculus framework. While an implementation of an axiomatic system is useful because it allows thinking about programs in terms of mathematical expressions, it may or may not always be practical. Many languages choose to incorporate functional elements without strictly adhering to functional doctrine. Many such languages (like Common Lisp) don't require variables to be final - you can modify things in place. They also don't require functions to depend only on their arguments - functions are allowed to access state outside of their boundaries. But they do include functional features - like higher order functions. Passing functions around in impure languages is a little bit different than doing it in the confines of lambda calculus and requires support for an interesting feature often referred to as lexical closure. Let's take a look at some sample code. Remember, in this case variables aren't final and functions can refer to variables outside of their scope: 422 | 423 | ```java 424 | Function makePowerFn(int power) { 425 | int powerFn(int base) { 426 | return pow(base, power); 427 | } 428 | 429 | return powerFn; 430 | } 431 | 432 | Function square = makePowerFn(2); 433 | square(3); // returns 9 434 | ``` 435 | 436 | The function make-power-fn returns a function that takes a single argument and raises it to a certain power. What happens when we try to evaluate square(3)? The variable power isn't anywhere in scope of powerFn because makePowerFn has returned and its stack is long gone. How can square work, then? The language must, somehow, store the value of power somewhere for square to work. What if we create another function, cube, that raises something to the third power? The runtime must now store two copies of power, one for each function we generated using make-power-fn. The phenomenon of storing these values is called a closure. Closures don't only store arguments of a host function. For example, a closure can look like this: 437 | 438 | ```java 439 | Function makeIncrementer() { 440 | int n = 0; 441 | 442 | int increment() { 443 | return ++n; 444 | } 445 | } 446 | 447 | Function inc1 = makeIncrementer(); 448 | Function inc2 = makeIncrementer(); 449 | 450 | inc1(); // returns 1; 451 | inc1(); // returns 2; 452 | inc1(); // returns 3; 453 | inc2(); // returns 1; 454 | inc2(); // returns 2; 455 | inc2(); // returns 3; 456 | ``` 457 | 458 | The runtime manages to store n, so incrementers can access it. Furthermore, it stores various copies, one for each incrementer, even though they're supposed to disappear when makeIncrementer returns. What does this code compile to? How do closures work behind the scenes? Fortunately, we have a back stage pass. 459 | 460 | A little common sense goes a long way. The first observation is that local variables are no longer limited to simple scope rules and have an undefined lifetime. The obvious conclusion is that they're no longer stored on the stack - they must be stored on the heap instead8. A closure, then, is implemented just like a function we discussed earlier, except that it has an additional reference to the surrounding variables: 461 | 462 | ```java 463 | class some_function_t { 464 | SymbolTable parentScope; 465 | 466 | // ... 467 | } 468 | ``` 469 | 470 | When a closure references a variable that's not in its local scope, it consults this reference for a parent scope. That's it! Closures bring functional and OO worlds closer together. Every time you create a class that holds some state and pass it to somewhere else, think of closures. A closure is just an object that creates "member variables" on the fly by grabbing them from the scope, so you don't have to! 471 | 472 | ### What's next? 473 | This article only scratches the surface of functional programming. Sometimes a small scratch can progress to something bigger and in our case it's a good thing. In the future I plan to write about category theory, monads, functional data structures, type systems in functional languages, functional concurrency, functional databases and much more. If I get to write (and in the process learn) about half of these topics my life will be complete. In the meantime, Google is our friend. 474 | 475 | ### Comments? 476 | If you have any questions, comments, or suggestions, please drop a note at coffeemug@gmail.com. I'll be glad to hear your feedback. 477 | 478 | 1When I was looking for a job in the fall of 2005 I often did ask this question. It's quite amusing how many blank stares I got. You would think that at about $300,000 a piece these people would at least have a good understanding of most tools available to them. 479 | 480 | 2This appears to be a controversial question. Physicists and mathematicians are forced to acknowledge that it isn't at all clear whether everything in the universe obeys the laws that can be described by mathematics. 481 | 482 | 3I've always hated history lessons that offer a dry chronology of dates, names, and events. To me history is about the lives of people who changed the world. It is about their private reasons behind their actions, and the mechanisms by which they affected millions of souls. For this reason this history section is hopelessly incomplete. Only very relevant people and events are discussed. 483 | 484 | 4When I was learning about functional programming I was very annoyed by the term "lambda" because I couldn't really understand what it really means. In this context lambda is a function, the single Greek letter was just easier to write in a mathematical notation. Every time you hear "lambda" when talking about functional programming just translate it in your mind to "function". 485 | 486 | 5Interestingly Java strings are immutable anyway. It's rather interesting to explore the reasons for this treachery, but that would distract us from our goal. 487 | 488 | 6Most functional language compilers optimize recursive functions by transforming them to their iterative alternatives whenever possible. This is called a tail call optimization. 489 | 490 | 7The opposite isn't always true. While it is sometimes possible to prove that two pieces of code are equivalent, it isn't possible in all situations. 491 | 492 | 8This is actually no slower than storing on the stack because once you introduce a garbage collector, memory allocation becomes an O(1) operation. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 傻瓜函数编程 2 | ============================================ 3 | 4 | 这篇文章是原文《[Functional Programming For The Rest of Us](http://www.defmacro.org/ramblings/fp.html)》的中文翻译。 5 | 6 | 因为懒,在知道有这篇文章的时候我Google了若干篇中文翻译版,打算在吃午饭的时候随便扫扫了解一下。可是偏偏运气不太好,搜到的几篇翻译质量都不太理想:有把“Is it edible”被翻译成“它可以被掌握吗”的,有把“calculus”翻译成“微积分”的,几乎所有的版本都把“With any luck your coworkers will start making fun of you for your FP comments in no time” 译为“很快你的同事就会开始取笑你对函数式编程的观点了”……读起来矛盾拗口让人担心会不会有其他地方出错引起误解,让人不敢再往下看。 7 | 8 | 看来还是“自己动手,丰衣足食”,我最终还是开始自己翻译这篇文章。 9 | 10 | 这个版本在追求“信达雅”的同时,希望在不改变原意的前提下用中文语气/语境来进行本土化表达。唯一的例外是专业术语,如continuation,关于术语的翻译有很多讨论,我倾向于不翻译直接用,这样对以后写程序以及和国际友人交流都有帮助。 11 | 12 | 如果您在阅读之后没有明显感到央视翻译腔的话,我的目的就算达到了。非常期待您的任何反馈。 13 | 对了,在翻译的同时,我还把这篇文章转成Markdown的格式以图更好的保存和传播。You are welcome Slava ;) 14 | 15 | 译者 于 2013年 16 | --------------------------------------------------------------------------------