1.3 函数式编程

我已经提过几次,Scala可以用作函数式编程语言。我想花几页的篇幅给你一些函数式编程的感觉。让我们从对比Java编程的命令式风格开始吧!如果我们想找到给定日期的最高气温,可能写出这样的Java代码:

  1. //Java code
  2. public static int findMax(List<Integer> temperatures) {
  3. int highTemperature = Integer.MIN_VALUE;
  4. for(int temperature : temperatures) {
  5. highTemperature = Math.max(highTemperature, temperature);
  6. }
  7. return highTemperature;
  8. }

我们创建了一个可变的变量highTemperature,在循环中不断修改它。当你拥有可变变量时,你就必须保证正确地初始化它们,在正确的地方将它们改成正确的值。

函数式编程是声明式风格,使用这种风格,你要说明做什么,而不是如何去做。如果你用过XSLT,规则引擎,或是ANTLR,那么你就已经用过函数式风格了。我们用函数式风格重写上面的代码,不用可变变量,如下代码所示:

Introduction/FindMaxFunctional.scala

  1. def findMax(temperatures : List[Int]) = {
  2. temperatures.foldLeft(Integer.MIN_VALUE) { Math.max }
  3. }

上面代码里,你看到了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

  1. val values = List(1, 2, 3, 4, 5)
  2. val doubleValues = values.map(_ * 2)

关键字val理解为“不变的”。我们告诉Scala,变量valuesdoubleValues一旦创建就不会改变。

尽管看上去不像,但_*2确实是一个函数。它是个匿名函数,这表示这个函数只有函数体,而没有函数名。下划线(_)表示传给这个函数的参数。函数本身作为参数传给map函数。map()函数在容器上迭代,对于容器中的每个元素,都会调用以参数给出的匿名函数。其结果是创建一个新的List,包含的元素就是原List元素值的翻倍。

看见怎么把函数(这里就是把一个数翻倍)当作普通参数和变量了吧?在Scala里面,函数是一等公民。

因此,虽然获得了一个将原List元素值翻倍的List,但我们并没有修改任何变量和对象。这种不变的方式是一个关键概念,它让函数式编程成为一种非常有吸引力的并发编程风格。在函数式编程中,函数是纯粹的。它们产生的输出只是基于其接收到的输入,它们不会受任何状态影响或也不会影响任何状态,无论是全局还是局部的。