原文链接 http://nerd-is.in/2013-09/scala-learning-higher-order-functions/
原文发表于:http://nerd-is.in/2013-09/scala-learning-higher-order-functions/
在函数式编程语言中,函数是“头等公民”,可以像任何其他数据类型一样被传递和操作。
因为Scala混合了面向对象和函数式的特性,所以对Scala来说,函数是“头等公民”。
作为值的函数
1 2 3 |
import scala.math._ val fun = ceil _ // _将ceil方法转成了函数 |
在Scala中,无法直接操纵方法,只能直接操纵函数,所以需要使用_。
fun的类型是(Double)=>Double,意为接受Double参数并返回Double的函数。
能够对fun做的有:调用,传递。
1 2 3 4 5 |
val num = 3.14 fun(num) // 返回4.0,调用fun Array(3.14, 1.42, 2.0).map(fun) //返回Array(4.0, 2.0, 2.0),将fun作为变量传递 |
匿名函数
函数不一定需要名称:
1 |
(x: Double) => 3 * x // 该匿名函数将传给它的参数乘3 |
可以将匿名函数赋值给变量,也可以当参数传递。
带函数参数的函数
如何实现一个接受另一个函数为参数的函数:
1 |
def valueAtOneQuarter(f: (Double) => Double) = f(0.25) |
该函数的类型是: ((Double) => Double) => Double。
还有可以返回一个函数的函数:
1 2 3 4 5 |
def mulBy(factor: Double) = (x: Double) => factor * x // mulBy可以产出任何两个数相乘的函数 val quintuple = mulBy(5) // (x: Double) => 5 * x quintuple(20) // 5 * 20 |
这样接受函数参数,或者是返回函数的函数,被称为高阶函数(higher-order function)。
参数(类型)推断
前面有定义高阶函数 def valueAtOneQuarter(f: (Double) => Double) = f(0.25),
因为已知参数的类型,所以Scala会尽可能推断出类型,在传入参数时,可以省掉一些内容。
1 2 3 4 |
valueAtOneQuarter((x: Double) => 3 * x) // 完整写法 valueAtOneQuarter((x) => 3 * x) // 已知参数类型,可以省掉Double valueAtOneQuarter(x => 3 * x) // 只有一个参数时,可以省去() valueAtOneQuarter(3 * _) // 参数只在右侧出现一次,可以用_替换 |
闭包
闭包(closure)这个概念,虽然差不多能懂,但解释不清楚的感觉,参考一下闭包的维基百科。
SAM转换
在Scala中,要某个函数做某件事时,会传一个函数参数给它。
而在Java中,并不支持传送参数。通常Java的实现方式是将动作放在一个实现某接口的类中,
然后将该类的一个实例传递给另一个方法。很多时候,这些接口只有单个抽象方法(single abstract method),
在Java中被称为SAM类型。
举例,点击一个按钮时,增加一个计数器:
1 2 3 4 5 6 7 8 |
var counter = 0 val button = new JButton("Increment") button.addActionListener(new ActionListener { override def actionPerformed(event: ActionEvent) { count += 1 } }) |
这是非常常见的,给按钮添加监听器的代码。
其实只要给addActionListener传一个函数参数,也就能够实现一样的功能了。
1 |
button.addActionListener((event: ActionEvent) => counter += 1) |
为了使这个语法真的生效,需要提供一个隐式转换。
隐式转换将在21章详述。下面是简单的示例:
1 2 3 4 |
implicit def makeAction(action: (ActionEvent) => Unit) = new ActionListener { override def actionPerformed(event: ActionEvent) { action(event) } } |
将这个函数和界面代码放在一起,就可以在所有预期ActionListener对象的地方,传入(ActionEvent)=>Unit函数参数。
从上面的代码可以看出,隐式转换就是将一种类型自动转换成另外一种类型,是个函数。
因为在Scala中,函数是头等公民,所以隐式转换的作用也大大放大了。
柯里化(Currying)
柯里化的概念也请参考维基百科。
柯里化指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。
新的函数返回一个以原有第二个参数作为参数的函数。
1 2 3 4 5 6 |
def mulOneAtATime(x: Int) = (y: Int) => x * y // 计算两个数的乘积 mulOneAtATime(6)(7) // 多参数的写法 def mul(x: Int, y: Int) = x * y |
mulOneAtATime(6)返回的是函数(y: Int)=>6*y,再将这个函数应用到7,最终得到结果。
柯里化函数可以在Scala中简写:
1 |
def mulOneAtATime(x: Int)(y: Int) = x * y |
多参数是个虚饰,不是编程语言的根本性的特质。
可以利用柯里化把某个函数参数单独拎出来,提供更多用于类型推断的信息。
1 2 3 |
val a = Array("Hello", "World") val b = Array("hello", "world") a.corresponds(b)(_.equalsIgnoreCase(_)) |
corresponds的类型声明如下:
1 |
def corresponds[B](that: GenSeq[B])(p: (T, B) ⇒ Boolean): Boolean |
方法有两个参数,that序列和f函数,其中f函数有两个参数,第二个参数类型是与that序列一致的。
因为使用了柯里化,我们可以省去第二个参数中B的类型,因为从that序列中推断出B的类型。
于是,_equalsIgnoreCase(_)这个简写就符合参数的要求了。
控制抽象
Scala中,可以将一系列语句归组成不带参数也没有返回值的函数。
1 2 3 4 5 6 7 8 |
def runInThread(block: () => Unit) { new Thread { override def run() { block() } }.start() } // 调用 runInThread { () => println("Hi"); Thread.sleep(10000); println("Bye") } |
可以去掉调用中的()=>,在参数声明和调用该函数参数的地方略去(),保留=>。
1 2 3 4 5 6 7 8 |
def runInThread(block: => Unit) { new Thread { override def run () { block } }.start() } // 调用 runInThread { println("Hi"); Thread.sleep(10000); println("Bye") } |
Scala程序员可以构建控制抽象:看上去像是编程语言关键字的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def until(condition: => Boolean)(block: => Unit) { if (!condition) { block until(condition)(block) } } // 使用 var x = 10 until (x == 0) { x -= 1 println(x) } |
这样的函数参数专业术语叫做换名调用参数(常规的参数叫换值调用参数)。函数在调用时,换名调用参数的表达式不会被求值,表达式会被当做参数传递下去。
return表达式
一般不需要使用return来返回函数值。但是return可以用来从一个匿名函数中返回值给包含这个匿名函数的带名函数,对于控制抽象来说是很有用的。如果要在带名参数中使用return,需要在定义中给出返回类型。
1 2 3 4 5 6 7 8 |
def indexOf(str: String, ch: Char): Int = { var i = 0 util (i == str.length) { if ( str(i) == str.length) return i i += 1 } return -1 } |
在这里,util这个抽象控制中的return语句,会使外部的带名函数indexOf终止并且返回i的值。
这里控制流程的实现依赖于在匿名函数中return表达式抛出的特殊异常。如果这个异常在被送往带名函数前被捕获,那么就无法为带名函数返回值了,这一点需要注意。
本章练习参考。