1.3 函数式编程
我已经提过几次,Scala可以用作函数式编程语言。我想花几页的篇幅给你一些函数式编程的感觉。让我们从对比Java编程的命令式风格开始吧!如果我们想找到给定日期的最高气温,可能写出这样的Java代码:
//Java code
public static int findMax(List<Integer> temperatures) {
int highTemperature = Integer.MIN_VALUE;
for(int temperature : temperatures) {
highTemperature = Math.max(highTemperature, temperature);
}
return highTemperature;
}
我们创建了一个可变的变量highTemperature
,在循环中不断修改它。当你拥有可变变量时,你就必须保证正确地初始化它们,在正确的地方将它们改成正确的值。
函数式编程是声明式风格,使用这种风格,你要说明做什么,而不是如何去做。如果你用过XSLT,规则引擎,或是ANTLR,那么你就已经用过函数式风格了。我们用函数式风格重写上面的代码,不用可变变量,如下代码所示:
Introduction/FindMaxFunctional.scala
def findMax(temperatures : List[Int]) = {
temperatures.foldLeft(Integer.MIN_VALUE) { Math.max }
}
上面代码里,你看到了Scala的简洁和函数式编程风格的相互作用。这是段高密度的代码。用几分钟时间沉淀一下。
我们创建了一个函数findMax()
,接收一个不变的容器(temperatures
)为参数,表示温度值。圆括号和花括号之间的“=
”告诉Scala推演这个函数的返回类型(这里是Int
)。
在这个函数里,我们调用这个collection的foldLeft()
方法,对容器中的每个元素运用Math.max()
。正如你所知道的,java.lang.Math
类的max()
方法接收两个参数,就是我们要确定最大值的两个值。在上面的代码里,这两个参数是隐式传递的。max()
的第一个隐式参数是之前的高值,第二个参数是foldLeft()
正在迭代的容器中的当前元素。foldLeft()
取回调用max
的结果,这就是当前的高值,在接下来调用max()
时把它传进去,同下一个元素比较。foldLeft()
的参数就是高温的初始值。
foldLeft()
方法需要花些功夫来掌握。稍稍做个假设,把容器中的元素当作是站成一排的人,我们要找出年纪最大的人的年龄。我们在笔记上写上0,把它传给这排的第一个人。第一个丢弃这个笔记(因为他比0岁年龄大);用他的年龄20创建一个新的笔记;把它传给这排的下一个人。第二个人,他比20岁年轻,简单把笔记传给下一个挨着他的人。第三个人,32岁,丢弃这个笔记,创建一个新的传递下去。我们从最后一个人获得的笔记就会包含年纪最大的人的年龄。把这一系列过程可视化,你就知道foldLeft()
背后做了些什么。
上面的代码是不是感觉像喝了一小口红牛?Scala代码高度简洁,非常紧凑。你不得不花些功夫学习这个语言。但是,一旦你掌握了它,你就能够利用它的威力和表现力了。
我们来看另外一个函数式风格的例子。假定我们想要一个List
,其元素就是将原List
值的翻倍。我们不会对每个元素进行循环来实现,只要简单的说,我们要元素翻倍,让语言来循环,如下所示:
Introduction/DoubleValues.scala
val values = List(1, 2, 3, 4, 5)
val doubleValues = values.map(_ * 2)
关键字val
理解为“不变的”。我们告诉Scala,变量values
和doubleValues
一旦创建就不会改变。
尽管看上去不像,但_*2
确实是一个函数。它是个匿名函数,这表示这个函数只有函数体,而没有函数名。下划线(_
)表示传给这个函数的参数。函数本身作为参数传给map
函数。map()
函数在容器上迭代,对于容器中的每个元素,都会调用以参数给出的匿名函数。其结果是创建一个新的List
,包含的元素就是原List
元素值的翻倍。
看见怎么把函数(这里就是把一个数翻倍)当作普通参数和变量了吧?在Scala里面,函数是一等公民。
因此,虽然获得了一个将原List
元素值翻倍的List
,但我们并没有修改任何变量和对象。这种不变的方式是一个关键概念,它让函数式编程成为一种非常有吸引力的并发编程风格。在函数式编程中,函数是纯粹的。它们产生的输出只是基于其接收到的输入,它们不会受任何状态影响或也不会影响任何状态,无论是全局还是局部的。