快学Scala第12章----高阶函数
本章要点
- 在Scala中函数是“头等公民”,就和数字一样;
- 你可以创建匿名函数,通常还会把它们交给其他函数;
- 函数参数可以给出需要稍后执行的行为;
- 许多集合方法都接受函数参数,将函数应用到集合中的值;
- 有很多语法上的简写让你以简短且易读的方式表达函数参数;
- 你可以创建操作代码块的函数,它们看上去就像是内建的控制语句。
作为值的函数
在Scala中,你可以在变量中存放函数:
import scala.math._
val num = 3.14
val fun = ceil _
这段代码将num设为3.14, fun设为ceil函数。
说明: ceil函数后的 _ 意味着你确实指的是这个函数,而不是碰巧忘记给它传递参数。 从技术上讲, _ 将ceil方法转成了函数,在Scala中,你无法直接操纵方法,而只能直接操纵函数。
怎么使用函数:
- 调用它
- 传递它,存放在变量中,或者作为参数传递给另一个函数
fun(num) // fun是一个包含函数的变量,而不是一个固定的函数
Array(3.14, 1.42, 2.0).map(fun) // 将fun传递给另一个函数, Array(4.0, 2.0, 2.0)
匿名函数
在Scala中,你不需要给每个函数命名,它就是匿名函数:
(x: Double) => 3 * x
// 将这个函数存放在变量中
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(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)
valueAtOneQuarter(ceil _) // 1.0
valueAtOneQuarter(sqrt _) // 0.5
这里的参数是一个接受Double并返回Double的函数。而valueAtOneQuarter的函数类型是:((Double) => Double) => Double ; 一个接受函数参数的函数,它就称作高阶函数。例如valueAtOneQuarter。
高阶函数也可以产出另一个函数,即返回一个函数:
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)
参数(类型)推断
Scala有比较强大的参数推导:
def valueAtOneQuarter(f: (Double) => Double) = f(0.25)
valueAtOneQuarter( (x: Double) => 3 * x ) // 0.75
// 可以简单写成
valueAtOneQuarter( (x) => 3 * x ) // 0.75
// 只有一个参数的情况,还可以省却参数的括号:
valueAtOneQuarter( x => 3 * x ) // 0.75
// 如果参数在 => 右侧只出现一次,可以用 _ 替换它
valueAtOneQuarter( 3 * _ ) // 0.75
这些简写方式仅在参数类型已知的情况下有效:
val fun = 3 * _ // error
val fun = 3 * (_: Double) // OK
val fun: (Double) => Double = 3 * _ // OK
一些有用的高阶函数
(1 to 9).map(0.1 * _) // 应用于所有元素
(1 to 9).map("*" * _).foreach(println _)
(1 to 9).filter(_ % 2 == 0) // 2,4,6,8
// reduceLeft方法接受一个二元的函数,将它应用到序列中的所有元素,从左到右
(1 to 9).reduceLeft(_ * _) // 1*2*3*...*8*9
// 排序
"Mary has a little lamb".split(" ").sortWith(_.length < _.length)
闭包
函数可以在变量不再处于作用于内时被调用。这样的函数称为闭包, 闭包由代码和代码用到的任何非局部变量定义构成。 例如:
def mulBy(factor: Double) = (x: Double) => factor * x
// 如下调用
val triple = mulBy(3)
val half = mulBy(0.5)
println(triple(14) + " " + half(14)) // 42 7
mulBy首次调用时将参数变量factor设为3, 该变量在(x: Double) => factor * x 函数的函数体内被引用,该函数被存入triple。然后参数变量factor从运行时的栈上被弹出;
mulBy第二次被调用时,参数变量被设为了0.5, 该变量在(x: Double) => factor * x 函数的函数体内被引用,该函数被存入half 。
这样,每一个返回的函数都要自己的factor设置。在这里triple和half存储的函数访问了它们作用于范围外的变量。
SAM转换
在Scala中,你可以传递函数作为参数,而在Java中是不可以的(目前),其通常的做法是将动作放在一个实现某接口的类中,然后将该类的一个实例传递给另一个方法。在很多时候,这些接口都只有单个抽象方法(single abstract method), 简称SAM类型。 例如:
var counter = 0
val button = new JButton("Increment")
button.addActionListener(new ActionListener {
override def actionPerformed(event: ActionEvent) {
counter += 1
}
})
这里使用了样板代码,我们希望的是只传递一个函数给addActionListener就好了:
button.addActionListener((event: ActionEvent) => counter += 1)
为了启用这个语法,你需要提供一个隐士转换,因为addActionListener是Java的方法。
implicit def makeAction(action: ( (ActionEvent) => Unit ) ) = {
new ActionListener {
override def actionPerformed(event: ActionEvent) { action(event) }
}
}
只需要简单的把这个函数和你的界面代码放在一起就可以在需要传入ActionListener 对象的地方传入任何(ActionEvent) => Unit 类型的函数了。
柯里化
柯里化指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个原有的函数的第二个参数作为参数的函数。
def mul(x: Int, y: Int) = x * y
// 定一个接受一个参数,生成另一个接受一个参数的函数
def mulOneAtAtime(x: Int) = (y: Int) => x * y
mulOneAtAtime(6)(7)
这里mulOneAtAtime(6)会返回(y: Int) => 6 * y 类型的函数,然后该函数又被调用,最终计算出结果
Scala支持如下简写来定义这样的柯里化函数:
def mulOneAtAtime (x: Int) (y: Int) = x * y
一个典型应用:corresponds方法可以比较两个序列是否在某个条件下相同:
val a = Array("Hello", "World")
val b = Array("hello", "world")
a.corresponds (b) (_.equalsIgnoreCase(_))
这里corresponds 是一个柯里化的函数,定义:
def corresponds[B] (that: Seq[B]) (p: (A, B) => Boolean): Boolean
that序列和p函数是分开的两个柯里化的参数,类型推断器先推断出that是一个String类型的序列,也就是B是String,然后才能在p函数中推断出函数类型是(String, String) => Boolean。
控制抽象
对于一个没有参数也没有返回值的函数:
def runInThread(block: () => Unit) {
new Thread {
override def run() { block() }
}.start()
}
runInThread { () => println("Hi"); Thread.sleep(1000); println("Bye") }
() => 这样看上比不那么美观,要向省掉() => 可以使用换名调用表示方法:在参数声明和调用该函数参数的地方略去(),但保留 => :
def runInThread(block: => Unit) {
new Thread {
override def run() { block }
}.start()
}
runInThread { println("Hi"); Thread.sleep(1000); println("Bye") }
在例如:
def until(condition: => Boolean) (block: => Unit) {
if (!condition) {
block
until(condition) (block)
}
}
var x = 10
until (x == 0) {
x -= 1
println(x)
}
这样的函数参数叫做换名调用参数。和常规的参数不同,函数在被调用时,参数表达式不会被求值。
return表达式
在Scala中,函数的返回值就是函数体的值,即最后一个表达式的值。所有不需要使用return语句返回函数值。但是,你可以用return来从一个匿名函数中返回值给包含这个匿名函数的带名函数:
def indexOf(str: String, ch: Char): Int = {
var i = 0
until (i == str.length) {
if (str(i) == ch) return i
i += 1
}
return -1
}
如果在带名函数中使用return语句,需要给出它的返回类型。