5.7 Scala的函数式编程功能

前面说过,函数式编程的思维模式与命令式编程不同。这里介绍几个与函数式编程相关的主题:

  • 使用函数遍历集合;
  • 映射-过滤-归约设计模式;
  • 柯里化。

5.7.1 使用函数遍历集合

在函数式编程中,很少使用forwhile循环来遍历数组和集合,并在循环体中处理每个元素。相反,将对数组或集合实例调用一个在内部对其进行遍历的方法,这个方法将一个lambda函数作为参数,并对每个元素调用这个函数:

  1. var a = List[Int]($text-5, 10, 15, 20, 25)
  2. a.foreach((x: Int) => println("%03d".format(x)))

这将打印005010015020025,其中使用了Java类java.lang.String的方法format来确保打印出来的整数包含三位(不够时在前面添加零)。

5.7.2 映射-过滤-归约设计模式

与函数式编程相关的一种著名的设计模式是映射-过滤-归约。下面分别来介绍这些模式:

  • 映射;
  • 过滤;
  • 归约。

  • 映射——对数据进行变换

需要对数组或集合的每个元素都进行变换时,可使用方法map

  1. var a = List[Int]($text-1, 2, 3)
  2. var b = a.map((x: Int) => 2 * x)
  3. println(b)

这将打印List(2, 4, 6)

方法map将一个这样的lambda函数作为参数,即包含一个类型与数组或集合元素相同的输入参数。在这里,我们创建了一个lambda函数,它接受类型为Int的参数x,并返回2 * x。对于每个元素,函数map都将调用传入的函数,从而创建一个新的列表。

  • 过滤——过滤集合或数组中的元素

数组和集合类都实现了方法filter,使用它可将集合或数组中的元素剔除,这是通过传递一个函数实现的:这个函数返回一个布尔值,指出是否要将传递给它的元素保留在过滤后的列表或数组中:

  1. var a = List[Int]($text-100, 150, 200, 300)
  2. var b = a.filter((x: Int) => x > 150)
  3. println(b)

打印的结果为List(200, 300)。由于100150都不大于150,因此对于这些元素,传递给filter的函数将返回false,导致它们都不会添加到结果中。

  • 归约——执行计算

传递给方法reduce的lambda函数接受两个参数:初值和当前元素。返回的值将作为下次调用的初值。下面的示例将元素的值累加:

  1. var a = List[Int]($text-10, 20, 30, 40, 50)
  2. var b = a.reduce((x: Int, y: Int) => x + y)
  3. println(b)

这将打印结果150。再来看一个示例,它使用了Scala运算符max

  1. var a = List[Int]($text-100, 2, 30, 60, 555)
  2. var b = a.reduce((x: Int, y: Int) => x max y)
  3. println(b)

这将打印555。运算符max返回两个值中较大的那个。正如你可能预期的,Scala还提供了运算符min

5.7 Scala的函数式编程功能 - 图1 与本章介绍的其他所有运算符一样,在Int类中,运算符minmax也是以方法的方式实现的。

5.7.3 柯里化

在Scala中,可向方法或函数提供多个参数列表:

  1. class CurryingTest {
  2. def curryingMethod(a: Int, b: Int)(c: Int): Int = {
  3. a * b * c
  4. }
  5. }

这被称为柯里化(currying)。在上述方法定义中,有两组输入参数,其中一组包含参数ab,而另一组只包含参数c。调用这个方法时,要指定所有的参数,必须像下面这样做:

  1. var c = new CurryingTest()
  2. var result = c.curryingMethod(2, 3)(4)
  3. println(result)

与预期的一样,这将返回24。如果只能在程序中这样调用这个方法,柯里化将毫无意义。在需要将函数传递给方法或函数时(这在函数式编程中很常见),柯里化很有用。下面是一个这样的示例:

  1. def doCurrying(x: Int, fun: Int => Int): Int = {
  2. fun(x)
  3. }
  4. var result = doCurrying(30, c.curryingMethod(10, 20))
  5. println(result)

这将打印6000。下面来粗略地解释一下这些代码。

(1) 方法doCurrying接受两个参数。

  • x:一个Int实例。
  • fun:接受一个Int参数并返回一个Int实例的函数。

(2) 函数doCurrying调用函数fun,将x作为输入参数传递给它,并返回函数fun的值。

(3) 调用函数doCurrying时,Scala创建一个临时的匿名(意味着没有名称)函数,这个函数使用第一组参数(a=10b=20)调用传入的方法curryingMethod。创建的函数还需要一组参数(在这里,这组参数值只包含参数c),这样它才能执行curryingMethod。因此,生成的函数需要一个Int输入参数。

(4) 由于生成的匿名临时函数的签名与定义Int => Int兼容(接受一个Int输入参数并返回一个Int值),Scala编译器将这个生成的函数作为doCurrying的输入参数。

(5) 方法doCurrying执行传递给它的函数(这里是生成的匿名函数),并将x作为输入参数传递给它。至此,两组参数都有了,生成的匿名函数可以使用全部三个参数调用方法c.curryingMethod了。编译器知道这将返回一个Int,与doCurrying的返回类型相同。