作为值的函数
在Scala中,函数是“头等公民”,就和数字一样。你可以在变量中存放函数:
scala> import scala.math._
import scala.math._
scala> val num = 3.14
num: Double = 3.14
scala> val fun = ceil _
fun: Double => Double = <function1>
scala> fun(num)
res0: Double = 4.0
从技术上讲,_ 将ceil方法变成了函数。在Scala中,没法随意操作方法,只能操作函数。这个函数的类型是(Double) => Double
,中间有个箭头;对比而言ceil方法的类型是(Double)Double
,中间没有箭头。没法直接使用这样类型,不过可以在编译器和REPL消息中找到它们。
在一个预期需要函数的上下文里使用方法名时,_ 后缀不是必需的。例如, 如下代码是合法的。
scala> val f:(Double) => Double = ceil
f: Double => Double = <function1>
说明: ceil方法是scala.math这个包对象的方法。如果你有一个来自类的方法,将它变成函数的方式略微不同:
scala> val f = (_:String).charAt(_:Int) // 这是一个类型为(String, Int) => Char的函数
f: (String, Int) => Char = <function2>
// 或者,也可以指定函数的类型,而不是参数类型:
scala> val f:(String, Int) => Char = _.charAt(_)
f: (String, Int) => Char = <function2>
匿名函数
在Scala中,你无须给每一个函数命名,正如你无须给每个数字命名一样。以下是一个匿名函数:
scala> (x:Double) => 3 * x
res3: Double => Double = <function1>
当然可以将这个函数存放到变量中:
scala> val triple = (x:Double) => 3*x
triple: Double => Double = <function1>
scala> Array(2.0,3.0,4.0).map(triple)
res6: Array[Double] = Array(6.0, 9.0, 12.0)
// 也可以将函数参数包在花括号当中而不是用圆括号
scala> Array(2.0,3.0,4.0).map{ (x:Double) => 3 * x }
res5: Array[Double] = Array(6.0, 9.0, 12.0)
说明:任何以def定义的(不论在REPL中、类中、还是在对象中)都是方法,不是函数。
带函数参数的函数
scala> def func2(f:(Double) => Double)=f(0.25)
func2: (f: Double => Double)Double
scala> func2(ceil _)
res8: Double = 1.0
scala> func2(sqrt _)
res9: Double = 0.5
由于func2是一个接受函数参数的函数,因此它被称作 “高阶函数(higher-order function)”。
高阶函数也可以产出另一个函数,如下示例:
scala> def mulBy(factor:Double)=(x:Double) => factor * x
mulBy: (factor: Double)Double => Double
scala> val first = mulBy(5)
first: Double => Double = <function1>
scala> first(20)
res10: Double = 100.0
mulBy(5)返回函数(x:Double) => 5 * x
。
mulBy函数有一个类型为Double的参数,返回一个类型为(Double) => Double
的函数。因此,它的类型为:(Double) => ((Doubel) => Doubel)
。
参数(类型)推断
当你将一个匿名函数传递给另一个函数或方法时,Scala会尽可能帮助你推断出类型信息。如下示例:
scala> def func3(f:(Double) => Double)=f(0.25)
func3: (f: Double => Double)Double
scala> func3((x:Double) => 3 * x)
res11: Double = 0.75
scala> func3((x) => 3 * x)
res12: Double = 0.75
scala> func3(x => 3 * x)
res13: Double = 0.75
// 如果参数在 => 右侧只出现一次,你可以用 _ 替换掉它:
scala> func3(3 * _)
res14: Double = 0.75
注意这些简写方式仅在参数类型已知的情况下有效:
scala> val fun1 = 3 * _ // 错误,无法推断出类型
<console>:17: error: missing parameter type for expanded function ((x$1) => 3.*(x$1))
val fun1 = 3 * _
^
scala> val fun1 = 3 * (_:Double)
fun1: Double => Double = <function1>
scala> val fun1: (Double) => Double = 3 * _ // 给出了fun1的类型
fun1: Double => Double = <function1>
说明:给出 _ 的类型有助于将方法变成函数。
scala> (1 to 9).reduceLeft(_ * _)
res15: Int = 362880
// 等同于:1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9
// 更加严格的来说:((((((((1 * 2) * 3) * 4) * 5) * 6) * 7) * 8) * 9)
scala> "Mary had a little lamb".split(" ").sortWith(_.length < _.length)
res16: Array[String] = Array(a, had, Mary, lamb, little)
闭包
在Scala中,可以在任何作用域内定义函数:包、类、甚至是另一个函数或方法。
scala> def mulBy(factor:Double)=(x:Double) => factor * x
mulBy: (factor: Double)Double => Double
scala> val triple = mulBy(3)
triple: Double => Double = <function1>
scala> val half = mulBy(0.5)
half: Double => Double = <function1>
scala> println(s"${triple(14)} ${half(14)}")
42.0 7.0
我们看一下:
- mulBy的首次调用将参数变量factor设为3。该变量在
(x:Double) => factor * x
函数的函数体内被引用,该函数被存入triple。然后参数变量factor从运行时的栈上被弹出。 - 接下来,mulBy再次被调用,这次factor被设为0.5,该变量在
(x:Double) => factor * x
函数的函数体内被引用,该函数被存入half。
每一个返回的函数都有自己的factor设置。这样一个函数被称作“闭包(closure)”。闭包由代码和代码用到的任何非局部变量定义构成。
这些函数实际上是以类的对象方式实现的,该类有一个实例变量factor和一个包含了函数体的apply方法。
说明:Java从Java8开始通过lambda表达式的形式也支持了闭包。
SAM转换
在Scala中, 每当你想要告诉另一个函数做某件事时,你都会传一个函数参数给它。Java8之前,要达到同样的目的, Java只能针对这个动作定义一个类和方法。
在Java8中,我们可以用lambda表达式来指定这样的动作。lambda表达式跟Scala的函数关系密切。幸运的是,这意味着Scala 2.12开始,我们可以将Scala函数传给预期接受一个“SAM接口”,任何带有单个抽象方法(single abstract method)的Java接口(Java代码)(Java中这类接口的正式名称为“函数式接口(functional interface)”)。
需要注意的是,从Scala 函数到Java SAM接口的转换只对函数字面量(function literal)有效,对持有函数值的变量没有用。
柯里化
柯里化(currying,以逻辑学家Haskell Brooks Curry 的名字命名)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。如下函数接受两个参数:
scala> val mul = (x:Int, y:Int) => x*y
mul: (Int, Int) => Int = <function2>
以下函数接受一个参数,生成另一个接受单个参数的函数:
scala> val muls = (x:Int) => ((y:Int) => x*y)
muls: Int => (Int => Int) = <function1>
scala> muls(2)(5)
res18: Int = 10
严格地讲,mul(2)的结果是函数(y:Int) => 2 * y
。而这个函数又被应用到5,因此最终输出10。
Scala支持如下简写来定义这样的柯里化函数:
scala> def muls(x:Int)(y:Int) = x*y
muls: (x: Int)(y: Int)Int
说明:任何以def定义的(不论在REPL 中、类中、还是在对象中)都是方法,不是函数。你在用def定义柯里化方法时,可以用多组圆括号。
注意方法类型(x: Int)(y: Int)Int。与此相对应地,当你定义函数时,必须用多个箭头,而不是多组园括号:
scala> val muls = (x:Int) => (y:Int) => x*y
muls: Int => (Int => Int) = <function1>
控制抽象
在Scala中,我们可以将一系列语句归组成不带参数也没有返回值的函数。举例来说,如下函数在线程中执行某段代码:
scala> def runInThread(block: () => Unit){
| new Thread{
| override def run(){ block() }
| }.start()
| }
runInThread: (block: () => Unit)Unit
这段代码以类型为() => Unit
的函数的形式给出。不过,当你调用该函数时,需要写一段不那么美观的() =>
:
scala> runInThread{() => println("Scala");Thread.sleep(1000);println("Bye")}
Scala
scala> Bye
要想在调用中省掉() =>
,可以使用换名(call-by-name)调用表示法:在参数声明
和调用该函数参数的地方略去()
,但保留=>
:
scala> def runInThread(block: => Unit){
| new Thread{
| override def run(){ block }
| }.start()
| }
runInThread: (block: => Unit)Unit
于是调用代码就变成了只是:
scala> runInThread{println("Scala");Thread.sleep(100);println("Bye")}
Scala
scala> Bye
Scala程序员可以构建控制抽象(control abstraction): 看上去像是编程语言的关键字的函数。举例来说,我们可以实现一个用起来完全像是在使用while语句那样的函数;或者,我们也可以再发挥一下,定义一个until语句,工作原理类似于while ,只不过把条件反过来用:
scala> def until(condition: => Boolean)(block: => Unit){
| if(!condition){
| block
| until(condition)(block)
| }
| }
until: (condition: => Boolean)(block: => Unit)Unit
以下是使用until的示例:
scala> until(x==0){
| x -= 1
| print(x + " ")
| }
9 8 7 6 5 4 3 2 1 0
这样的函数参数有一个专业术语,叫作换名调用参数。和一个常规(或者说换值调用)的参数不同,函数在被调用时,参数表达式不会被求值;毕竟,在调用until时,我们并不希望x==0
被求值得到false;与之相反,表达式成为无参函数的函数体,而该函数被当作参数传递下去。
仔细看一下until函数的定义。注意它是柯里化的:函数首先处理掉condition,然后把block当作完全独立的另一个参数。如果没有柯里化,调用就会变成这个样子:
until (x == 0 , { .. . } )
return表达式
在Scala中,你无须用return语句来返回函数值,函数的返回值就是函数体的值。不过,你可以用return来从一个匿名函数中返回值给包含这个匿名函数的带名函数。这对于控制抽象是很有用的。举例来说,如下函数:
scala> def indexOf(str:String, ch:Char):Int={
| var i = 0
| until(i==str.length){
| if(str(i)==ch) return i
| i += 1
| }
| return -1
| }
indexOf: (str: String, ch: Char)Int
在这里,匿名函数 { if(str(i)==ch) return i; i += 1 }
被传递给until,当return表达式被执行时,包含它的带名函数indexOf终止并返回给定的值。
如果你要在带名函数中使用return的话,则需要给出其返回类型。举例来说,在上述indexOf函数中,编译器没法推断出它会返回Int。
控制流程的实现依赖于一个在匿名函数的return表达式中抛出的特殊异常,该异常从until函数传出,并被indexOf函数捕获。
注意:如果异常在被送往带名函数值前,在一个try代码块中被捕获了,那么相应的值就不会被返回。
参考:快学scala(第二版)