Scala中的高阶函数

文/霍斯曼


Scala混合了面向对象和函数式的特性。在函数式编程语言中,函数是“头等公民”,可以像任何其他数据类型一样被传递和操作。每当你想要给算法传入明细动作时这个特性就会变得非常有用。在函数式编程语言中,你只需要将明细动作包在函数当中作为参数传入即可。在本文中,你将会看到如何通过那些使用或返回函数的函数来提高我们的工作效率。


作为值的函数
在Scala中,函数是“头等公民”,就和数字一样。你可以在变量中存放函数:
import scala.math._
val num = 3.14
val fun = ceil _
这段代码将num设为3.14,fun设为ceil函数。
ceil函数后的_意味着你确实指的是这个函数,而不是碰巧忘记了给它送参数。
说明:从技术上讲,_将ceil方法转成了函数。在Scala中,你无法直接操纵方法,而只能直接操纵函数。
当你在REPL中尝试这段代码时,并不意外,num的类型是Double。fun的类型被报告为(Double) => Double,也就是说,接受并返回Double的函数。
你能对函数做些什么呢?两件事:
调用它。
传递它,存放在变量中,或者作为参数传递给另一个函数。
以下是如何调用存放在fun中的函数:
fun(num) // 4.0
正如你所看到的,这里用的是普通的函数调用语法。唯一的区别是,fun是一个包含函数的变量,而不是一个固定的函数。
以下是如何将fun传递给另一个函数:
Array(3.14, 1.42, 2.0).map(fun) // Array(4.0, 2.0, 2.0)
map方法接受一个函数参数,将它应用到数组中的所有值,然后返回结果的数组。在本章中,你将会看到许多其他接受函数参数的方法。
匿名函数
在Scala中,你不需要给每一个函数命名,正如你不需要给每个数字命名一样。以下是一个匿名函数:
(x: Double) => 3 * x
该函数将传给它的参数乘以3。
你当然可以将这个函数存放到变量中:
val triple = (x: Double) => 3 * x
这就跟你用def一样:
def triple(x: Double) = 3 * x
但是你不需要给函数命名。你可以直接将它传递给另一个函数:
Array(3.14, 1.42, 2.0).map((x: Double) => 3 * x)
// Array(9.42, 4.26, 6.0)
在这里,我们告诉map方法:“将每个元素乘以3”。
说明:如果你愿意,也可以将函数参数包在花括号当中而不是用圆括号,例如:
Array(3.14, 1.42, 2.0).map{ (x: Double) => 3 * x }
在使用中置表示法时(没有句点),这样的写法比较常见。
Array(3.14, 1.42, 2.0) map { (x: Double) => 3 * x }
带函数参数的函数
在本节中,你将会看到如何实现接受另一个函数作为参数的函数。以下是一个示例:
def valueAtOneQuarter(f: (Double) => Double) = f(0.25)
注意,这里的参数可以是任何接受Double并返回Double的函数。valueAtOneQuarter函数将计算那个函数在0.25位置的值。
例如:
valueAtOneQuarter(ceil _) // 1.0
valueAtOneQuarter(sqrt _) // 0.5 (因为 0.5 × 0.5 = 0.25)
valueAtOneQuarter的类型是什么呢?它是一个带有单个参数的函数,因为它的类型写做:
(参数类型) => 结果类型
结果类型很显然是Double,而参数类型已经在函数头部以 (Double) => Double给出了。因此,valueAtOneQuarter的类型为:
((Double) => Double) => Double
由于valueAtOneQuarter是一个接受函数参数的函数,因此它被称做高阶函数(higher-order function)。
高阶函数也可以产出另一个函数。以下是一个简单示例:
def mulBy(factor: Double) = (x: Double) => factor * x
举例来说,mulBy(3)返回函数(x: Double) => 3 * x,这个函数在前一节你已经见过了。mulBy的威力在于,它可以产出能够乘以任何数额的函数:
val quintuple = mulBy(5)
quintuple(20) // 100
mulBy函数有一个类型为Double的参数,返回一个类型为 (Double) => Double 的函数。因此,它的类型为:
(Double) => ((Double) => Double)
参数(类型)推断
当你将一个匿名函数传递给另一个函数或方法时,Scala会尽可能帮助你推断出类型信息。举例来说,你不需要将代码写成:
valueAtOneQuarter((x: Double) => 3 * x) // 0.75
由于valueAtOneQuarter方法知道你会传入一个类型为 (Double) => Double 的函数,你可以简单地写成:
valueAtOneQuarter((x) => 3 * x)
作为额外奖励,对于只有一个参数的函数,你可以略去参数外围的():
valueAtOneQuarter(x => 3 * x)
这样更好了。如果参数在=>右侧只出现一次,你可以用_替换掉它:
valueAtOneQuarter(3 * _)
从舒适度上讲,这是终极版本了,并且阅读起来也很容易:一个将某值乘以3的函数。
请注意这些简写方式仅在参数类型已知的情况下有效。
val fun = 3 * _ // 错误:无法推断出类型
val fun = 3 * (_: Double) // OK
val fun: (Double) => Double = 3 * _ // OK,因为我们给出了fun的类型
当然,最后一个定义很造作。不过它展示了函数是如何被作为参数(刚好是那个类型)传入的。
一些有用的高阶函数
要熟悉和适应高阶函数,一个不错的途径是练习使用Scala集合库中的一些常用的(且显然很有用的)接受函数参数的方法。
你已经见过map方法了,这个方法将一个函数应用到某个集合的所有元素并返回结果。以下是一个快速地产出包含0.1,0.2,…,0.9的集合的方式:
(1 to 9).map(0.1 * _)
说明:这里有一个通用的原则。如果你要的是一个序列的值,那么想办法从一个简单的序列转化得出。
让我们用它来打印一个三角形:
(1 to 9).map(“*” * _).foreach(println _)
结果是:
*
**
***
****
*****
******
*******
********
*********
在这里,我们还用到了foreach,它和map很像,只不过它的函数并不返回任何值。foreach只是简单地将函数应用到每个元素而已。
filter方法输出所有匹配某个特定条件的元素。举例来说,以下是如何得到一个序列中的所有偶数:
(1 to 9).filter(_ % 2 == 0) // 2, 4, 6, 8
当然,这并不是得到该结果最高效的方式。
reduceLeft方法接受一个二元的函数——即一个带有两个参数的函数——并将它应用到序列中的所有元素,从左到右。例如:
(1 to 9).reduceLeft(_ * _)
等同于
1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9
或者,更加严格地说
(…((1 * 2) * 3) * … * 9)
注意乘法函数的紧凑写法_ * _,每个下画线分别代表一个参数。
你还需要一个二元函数来做排序。例如:
“Mary has a little lamb”.split(“ ”).sortWith(_.length < _.length)
输出一个按长度递增排序的数组:Array(“a”, ”had”, ”Mary”, ”lamb”, ”little”)。
作者Cay S. Horstmann,是《Java核心技术》卷1和卷2第8版(Sun Microsystems出版社2008年出版)的主要作者,除此之外,他还著有其他十多本面向专业程序员和计算机科学专业学生的书籍。他是San Jose州立大学计算机科学专业的教授,同时还是一位Java Champion。
本文节选自《快学Scala》一书。本书由(美)霍斯曼(Horstmann,C.S.)著,高宇翔译,由电子工业出版社出版。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值