scala函数式编程
本篇博客内容完全是摘自 IBM developerWorks,非原创,感觉文章不错,遂转至本博客。原文 http://www.ibm.com/developerworks/cn/java/j-lo-funinscala3/
Scala 是一种有趣的语言。它一方面吸收继承了多种语言中的优秀特性,一方面又没有抛弃 Java 这个强大的平台,它运行在 JVM 之上,轻松实现和丰富的 Java 类库互联互通。它既支持面向对象的编程方式,又支持函数式编程。它写出的程序像动态语言一样简洁,但事实上它却是严格意义上的静态语言。Scala 就像一位武林中的集大成者,将过去几十年计算机语言发展历史中的精萃集于一身,化繁为简,为程序员们提供了一种新的选择。作者希望通过这个系列,可以为大家介绍 Scala 语言的特性,和 Scala 语言给我们带来的关于编程思想的新的思考。本文将带领大家一起回顾函数式编程的历史,清楚函数式编程的定义,并以一个例子,由易到难为大家展示函数式编程的优点,最后介绍了柯里化的概念。
函数式编程是这几年很受欢迎的一个话题,即使你是一个刚刚踏入职场的新人,如果在面试时能有意无意地透露出你懂那么一点点函数式编程,也会让你的面试官眼前一亮。然而函数式编程并不是一个新的概念,它的源头可以追溯到计算机尚未发明之前。本文将带领大家回顾一下函数式编程的历史,并使用 Scala 语言为大家讲解函数式编程的基本概念。
函数式编程的历史
有机会看到这篇文章的读者,大概都会知道阿兰·图灵(Alan Turing)和约翰·冯·诺伊曼(John von Neumann)。阿兰·图灵提出了图灵机的概念,约翰·冯·诺伊曼基于这一理论,设计出了第一台现代计算机。由于图灵以及冯·诺伊曼式计算机的大获成功,历史差点淹没了另外一位同样杰出的科学家和他的理论,那就是阿隆佐·邱奇(Alonzo Church)和他的λ演算。阿隆佐·邱奇是阿兰·图灵的老师,上世纪三十年代,他们一起在普林斯顿研究可计算性问题,为了回答这一问题,阿隆佐·邱奇提出了λ演算,其后不久,阿兰·图灵提出了图灵机的概念,尽管形式不同,但后来证明,两个理论在功能上是等价的,条条大路通罗马。如果不是约翰·麦卡锡(John McCarthy),阿隆佐·邱奇的λ演算恐怕还要在历史的故纸堆中再多躺几十年,约翰·麦卡锡是人工智能科学的奠基人之一,他发现了λ演算的珍贵价值,发明了基于λ演算的函数式编程语言:Lisp,由于其强大的表达能力,一推出就受到学术界的热烈欢迎,以至于一段时间内,Lisp 成了人工智能领域的标准编程语言。很快,λ演算在学术界流行开来,出现了很多函数式编程语言:Scheme 、SML、Ocaml 等,但是在工业界,还是命令式编程语言的天下,Fortran、C、C++、Java 等。随着时间的流逝,越来越多的计算机从业人员认识到函数式编程的意义,爱立信公司于上世纪八十年代开发出了 Erlang 语言来解决并发编程的问题;在互联网的发展浪潮中,越来越多的语言也开始支持函数式编程:JavaScript、Python、Ruby、Haskell、Scala 等。可以预见,如果过去找一个懂什么是函数式编程的程序员很困难,那么在不久的将来,找一个一点也没听过函数式编程的程序员将更加困难。
什么是函数式编程
狭义地说,函数式编程没有可变的变量、循环等这些命令式编程方式中的元素,像数学里的函数一样,对于给定的输入,不管你调用该函数多少次,永远返回同样的结果。而在我们常用的命令式编程方式中,变量用来描述事物的状态,整个程序,不过是根据不断变化的条件来维护这些变量。
广义地说,函数式编程重点在函数,函数是这个世界里的一等公民,函数和其他值一样,可以到处被定义,可以作为参数传入另一个函数,也可以作为函数的返回值,返回给调用者。利用这些特性,可以灵活组合已有函数形成新的函数,可以在更高层次上对问题进行抽象。本文的重点将放在这一部分。
函数式编程有什么优点
约翰·巴克斯(John Backus)为人熟知的两项成就是 FORTRAN 语言和用于描述形式系统的巴克斯范式,因为这两项成就,他获得了 1977 年的图灵奖。有趣的是他在获奖后,做了一个关于函数式编程的讲演:Can Programming Be Liberated From the von Neumann Style? 1977 Turing Award Lecture。他认为像 FORTRAN 这样的命令式语言不够高级,应该有新的,更高级的语言可以摆脱冯诺依曼模型的限制,于是他又发明了 FP 语言,虽然这个语言未获成功,但是约翰·巴克斯关于函数式编程的论述却得到了越来越多的认可。下面,我们就罗列一些函数式编程的优点。
首先,函数式编程天然有并发的优势。由于工艺限制,摩尔定律已经失效,芯片厂商只能采取多核策略。程序要利用多核运算,必须采取并发,而并发最头疼的问题就是共享数据,狭义的函数式编程没有可变的变量,数据只读不写,并发的问题迎刃而解。这也是前面两篇文章中,一直建议大家在定义变量时,使用 val 而不是 var 的原因。爱立信公司发明的 Erlang 语言就是为解决并发的问题而生,在电信行业取得了不俗的成绩。
其次,函数式编程有迹可寻。由于不依赖外部变量,给定输入函数的返回结果永远不变,对于复杂的程序,我们可以用值替换的方式(substitution model)化繁为简,轻松得出一段程序的计算结果。为这样的程序写单元测试也很方便,因为不用担心环境的影响。
最后,函数式编程高屋建瓴。写程序最重要的就是抽象,不同风格的编程语言为我们提供了不同的抽象层次,抽象层次越高,表达问题越简洁,越优雅。读者从下面的例子中可以看到,使用函数式编程,有一种高屋建瓴的感觉。
抽象,抽象,再抽象!
说了这么多,相信很多性急的读者都等不及想看看怎么使用 Scala 进行函数式编程了吧。那么,先请大家暂时忘掉以前命令式编程的经验,用一个全新的大脑来开始这段函数式编程之旅。
故事从我上初中的外甥小龙身上开始,像所有聪明的孩子一样,小龙身上具备了懒,不耐烦以及妄自尊大这些优秀特质。他厌倦了数学作业上那些大量没有意义的,重复的练习题。还好他有个当程序员的姨夫:在电脑上装个 Scala,写程序做吧。于是小龙把 Scala 当作一个计算器,写出了他有生以来第一段程序:
清单 1. 求立方
35 * 35 * 35 68 * 68 * 68 // 以下省去大量重复的,没有意义的练习题
作业做完了,虽然大脑得到了休息,但是小龙的手累坏了!作为一个懒人,小龙是不会满足于不动脑,但要动手这种状况的。于是,我教给了他最基本的抽象方式:将算法抽象为一个函数。小龙很快做完作业,高高兴兴跟小伙伴们打篮球去了。
清单 2. 求立方函数
def cube(n: Int) = n * n * n // 有了这个函数,小龙做起作业轻松多了 cube(35) cube(68) // 以下省去大量重复的,没有意义的练习题
随着教学进度的加快,小龙的作业也越来越难了,很快,小龙遇到了这样的题目:求出 1 到 10 的立方和。聪明如小龙,或者说懒惰如小龙,在前一个函数基础之上,很快又定义了个新函数,还是个递归函数!没错,在小龙还没看见过循环之前,我先教会了他递归,他理解起来毫不费力:对 a 到 b 之间的数求立方和,等于 a 的立方和,加上 (a + 1) 到 b 之间的数的立方和。如果读者对于递归还有疑惑,请参考作者的前一篇文章《使用递归的方式去思考》。小龙又很快做完作业,高高兴兴跟着小伙伴们打球去了。
清单 3. 求立方和
def cube(n: Int) = n * n * n def sumCube(a: Int, b: Int): Int = if (a > b) 0 else cube(a) + sumCube(a + 1, b) // 有了这个函数,小龙做起作业轻松多了 sumCube(1, 10)
塞翁失马,焉知非福,好事很快变坏事,由于小龙数学作业做得又快又好,被老师选拔为奥数培养对象,除过作业,小龙每天还要做大量的额外练习:求 1 到 10 的和,求 1 到 10 的平方和,求 1 到 10 的阶乘和等等。这时,小龙已经对定义函数很熟练了,三下五除二,小龙又定义出一堆函数出来。
清单 4. 各种求和函数
def cube(n: Int) = n * n * n def id(n: Int) = n def square(n : Int) = n * n def fact(n: Int): Int = if (n == 0) 1 else n * fact(n - 1) def sumCube(a: Int, b: Int): Int = if (a > b) 0 else cube(a) + sumCube(a + 1, b) def sumSquare(a: Int, b: Int): Int = if(a > b) 0 else square(a) + sumSquare(a + 1, b) def sumFact(a: Int, b: Int): Int = if (a > b) 0 else fact(a) + sumFact(a + 1, b) def sumInt(a: Int, b: Int): Int = if(a > b) 0 else id(a) + sumInt(a + 1, b) // 有了这些函数,小龙做起作业轻松多了 sumCube(1, 10) sumInt(1, 10) sumSquare(1, 10) sumFact(1, 10)
问题解决了,但小龙总觉得哪里不对劲,(这时,一个画外音高喊:Don ’ t Repeat Yourself!),是的,仔细观察小龙定义的这四个求和函数,几乎是一模一样的。能不能也将这些一模一样的东西抽象出来?我觉得是时候教给他第二项本领了:高阶函数(Higher-Order Function),所谓高阶函数,就是操作其他函数的函数。以求和为例,我们可以定义一个新的求和函数,该函数接受另外一个函数作为参数,这个作为参数的函数代表了某种对数据的操作。使用高阶函数后,抽象层次提高,代码变得更简单了。
清单 5. 使用高阶函数定义求和函数
def cube(n: Int) = n * n * n def id(n: Int) = n def square(n : Int) = n * n def fact(n: Int): Int = if (n == 0) 1 else n * fact(n - 1) // 高阶函数 def sum(f: Int => Int, a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f, a + 1, b) // 使用高阶函数重新定义求和函数 def sumCube(a: Int, b: Int): Int = sum(cube, a, b) def sumSquare(a: Int, b: Int): Int = sum(square, a, b) def sumFact(a: Int, b: Int): Int = sum(fact, a, b) def sumInt(a: Int, b: Int): Int = sum(id, a, b) // 有了这些函数,小龙做起作业轻松多了 sumCube(1, 10) sumInt(1, 10) sumSquare(1, 10) sumFact(1, 10)
对于简单的函数,我们还可以将其转化为匿名函数,让程序变得更简洁一些。在高阶函数中使用匿名函数,这是函数式编程中经常用到的一个技巧,多数情况下,我们关心的是高阶函数,而不是作为参数传入的函数,所以为其单独定义一个函数是没有必要的。值得称赞的是 Scala 中定义匿名函数的语法很简单,箭头左边是参数列表,右边是函数体,参数的类型是可省略的,Scala 的类型推测系统会推测出参数的类型。使用匿名函数后,我们的代码变得更简洁了:
清单 6. 在高阶函数中使用匿名函数
def fact(n: Int): Int = if (n == 0) 1 else n * fact(n - 1) // 高阶函数 def sum(f: Int => Int, a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f, a + 1, b) // 使用高阶函数重新定义求和函数 def sumCube(a: Int, b: Int): Int = sum(x => x * x * x, a, b) def sumSquare(a: Int, b: Int): Int = sum(x => x * x, a, b) def sumFact(a: Int, b: Int): Int = sum(fact, a, b) def sumInt(a: Int, b: Int): Int = sum(x => x, a, b) // 有了这些函数,小龙做起作业轻松多了 sumCube(1, 10) sumInt(1, 10) sumSquare(1, 10) sumFact(1, 10)
小龙的故事到此就结束了,希望读者能从这一例子中,体会出函数式编程的一些精妙之处。下面我们将进入函数式编程的另一个概念:柯里化(Currying)
柯里化
作为一个程序员,应该永远有一颗追求完美的心,上面使用匿名函数后的高阶函数还有什么地方值得改进呢?希望大家还会想起那句话:Don ’ t Repeat Yourself !求和函数的两个上下限参数 a
,
b
被重复得传来传去。我们试着重新定义 sum
函数,让它接受一个函数作为参数,同时返回另外一个函数。看到没?使用新的 sum
函数,我们再定义各种求和函数时,完全不需要这两个上下限参数了,我们的程序又一次得到了简化。
清单 7. 返回函数的高阶函数
def fact(n: Int): Int = if (n == 0) 1 else n * fact(n - 1) // 高阶函数 def sum(f: Int => Int): (Int, Int) => Int = { def sumF(a: Int, b: Int): Int = if (a > b) 0 else f(a) + sumF(a + 1, b) sumF } // 使用高阶函数重新定义求和函数 def sumCube: Int = sum(x => x * x * x) def sumSquare: Int = sum(x => x * x) def sumFact: Int = sum(fact) def sumInt: Int = sum(x => x) // 这些函数使用起来还和原来一样 ! sumCube(1, 10) sumInt(1, 10) sumSquare(1, 10) sumFact(1, 10)
能不能再简单一点呢?既然 sum
返回的是一个函数,我们应该可以直接使用这个函数,似乎没有必要再定义各种求和函数了。
清单 8. 直接调用高阶函数
def fact(n: Int): Int = if (n == 0) 1 else n * fact(n - 1) // 高阶函数 def sum(f: Int => Int): (Int, Int) => Int = { def sumF(a: Int, b: Int): Int = if (a > b) 0 else f(a) + sumF(a + 1, b) sumF } // 这些函数没有必要了 //def sumCube: Int = sum(x => x * x * x) //def sumSquare: Int = sum(x => x * x) //def sumFact: Int = sum(fact) //def sumInt: Int = sum(x => x) // 直接调用高阶函数 ! sum(x => x * x * x) (1, 10) //=> sumCube(1, 10) sum(x => x) (1, 10) //=> sumInt(1, 10) sum(x => x * x) (1, 10) //=> sumSquare(1, 10) sum(fact) (1, 10) //=> sumFact(1, 10)
这种返回函数的高阶函数极为有用,因此 Scala 为其提供了语法糖,上面的 sum
函数可以简写为:
清单 9. 高阶函数的语法糖
// 没使用语法糖的 sum 函数 def sum(f: Int => Int): (Int, Int): Int = { def sumF(a: Int, b: Int): Int = if (a > b) 0 else f(a) + sumF(a + 1, b) sumF } // 使用语法糖后的 sum 函数 def sum(f: Int => Int)(a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f)(a + 1, b)
读者可能会问:我们把原来的 sum
函数转化成这样的形式,好处在哪里?答案是我们获得了更多的可能性,比如刚开始求和的上下限还没确定,我们可以在程序中把一个函数传给 sum
,sum(fact)
完全是一个合法的表达式,待后续上下限确定下来时,再把另外两个参数传进来。对于 sum 函数,我们还可以更进一步,把 a,b 参数再转化一下,这样 sum 函数就变成了这样一个函数:它每次只能接收一个参数,然后返回另一个接收一个参数的函数,调用后,又返回一个只接收一个参数的函数。这就是传说中的柯里化,多么完美的形式!在现实世界中,的确有这样一门函数式编程语言,那就是 Haskell,在 Haskell 中,所有的函数都是柯里化的,即所有的函数只接收一个参数!
清单 10. 柯里化
// 柯里化后的 sum 函数 def sum(f: Int => Int)(a: Int) (b: Int): Int = if (a > b) 0 else f(a) + sum(f)(a + 1)(b) // 使用柯里化后的高阶函数 ! sum(x => x * x * x)(1)(10) //=> sumCube(1, 10) sum(x => x)(1)(10) //=> sumInt(1, 10)
结束语
本文和大家一起回顾了函数式编程的历史,并使用了大量示例代码帮助大家理解函数式编程中的基本概念。在 Scala 类库中,使用函数式编程的例子比比皆是,特别是对于列表的操作,将高阶函数的优势展示得淋漓尽致,限于篇幅,不能在本文中为大家作以介绍,作者将在后面的系列文章中,以 Scala 中的列表为例,详细介绍高阶函数在实战中的应用。