【版权声明】博客内容由厦门大学数据库实验室拥有版权,未经允许,请勿转载!
[返回Spark教程首页]
Scala是一门多范式编程语言,混合了面向对象编程和函数式编程的风格。在过去很多年,面向对象编程一直是主流,但是,随着大数据时代的到来,函数式编程开始迅速崛起,因为,函数式编程可以较好满足分布式并行编程的需求(函数式编程一个重要特性就是值不可变性,这对于编写可扩展的并发程序而言可以带来巨大好处,因为它避免了对公共的可变状态进行同步访问控制的复杂问题)。
函数字面量
函数字面量可以体现函数式编程的核心理念。前面我们已经介绍过,字面量包括整数字面量、浮点数字面量、布尔型字面量、字符字面量、字符串字面量、符号字面量、函数字面量和元组字面量。
举例如下:
val i = 123 //123就是整数字面量
val i = 3.14 //3.14就是浮点数字面量
val i = true //true就是布尔型字面量
val i = 'A' //'A'就是字符字面量
val i = "Hello" //"Hello"就是字符串字面量
除了函数字面量我们会比较陌生以外,其他几种字面量都很容易理解。现在我们来认识什么是“函数字面量”。
在非函数式编程语言里,函数的定义包含了“函数类型”和“值”两种层面的内容。但是,在函数式编程中,函数是“头等公民”,可以像任何其他数据类型一样被传递和操作,也就是说,函数的使用方式和其他数据类型的使用方式完全一致了。这时,我们就可以像定义变量那样去定义一个函数,由此导致的结果是,函数也会和其他变量一样,开始有“值”。就像变量的“类型”和“值”是分开的两个概念一样,函数式编程中,函数的“类型”和“值”也成为两个分开的概念,函数的“值”,就是“函数字面量”。
函数的类型和值
下面我们一点点引导大家更好地理解函数的“类型”和“值”的概念。
我们现在定义一个大家比较熟悉的传统类型的函数,定义的语法和我们之前介绍过的定义“类中的方法”类似(实际上,定义函数最常用的方法是作为某个对象的成员,这种函数被称为方法):
def counter(value: Int): Int = { value += 1}
上面定义个这个函数的“类型”如下:
(Int) => Int
实际上,只有多个参数时(不同参数之间用逗号隔开),圆括号才是必须的,当参数只有一个时,圆括号可以省略,如下:
Int => Int
上面就得到了函数的“类型”,下面看看如何得到函数的“值”。
实际上,我们只要把函数定义中的类型声明部分去除,剩下的就是函数的“值”,如下:
(value) => {value += 1} //只有一条语句时,大括号可以省略
上面就是函数的“值”,需要注意的是,采用“=>”而不是“=”,这是Scala的语法要求。
现在,我们再按照大家比较熟悉的定义变量的方式,采用Scala语法来定义一个函数,如下:
声明一个变量时,我们采用的形式是:
val num: Int = 5 //当然,Int类型声明也可以省略,因为Scala具有自动推断类型的功能
照葫芦画瓢,我们也可以按照上面类似的形式来定义Scala中的函数:
val counter: Int => Int = { (value) => value += 1 }
从上面可以看出,在Scala中,函数已经是“头等公民”,单独剥离出来了“值”的概念,一个函数“值”就是函数字面量。这样,我们只要在某个需要声明函数的地方声明一个函数类型,在调用的时候传一个对应的函数字面量即可,和使用普通变量一模一样。
匿名函数、Lambda表达式与闭包
我们不需要给每个函数命名,这时就可以使用匿名函数,如下:
(num: Int) => num * 2
上面这种匿名函数的定义形式,我们经常称为“Lambda表达式”。“Lambda表达式”的形式如下:
(参数) => 表达式 //如果参数只有一个,参数的圆括号可以省略
我们可以直接把匿名函数存放到变量中,下面是在Scala解释器中的执行过程:
scala> val myNumFunc: Int=>Int = (num: Int) => num * 2 //这行是我们输入的命令,把匿名函数定义为一个值,赋值给myNumFunc变量
myNumFunc: Int => Int = <function1> //这行是执行返回的结果
scala> println(myNumFunc(3)) //myNumFunc函数调用的时候,需要给出参数的值,这里传入3,得到乘法结果是6
6
实际上,Scala具有类型推断机制,可以自动推断变量类型,比如下面两条语句都是可以的:
val number: Int = 5
val number =5 //省略Int类型声明
所以,上面的定义中,我们可以myNumFunc的类型声明,也就是去掉“Int=>Int”,在Scala解释器中的执行过程如下:
scala> val myNumFunc = (num: Int) => num * 2
myNumFunc: Int => Int = <function1>
scala> println(myNumFunc(3))
6
下面我们再尝试一下,是否可以继续省略num的类型声明,在Scala解释器中的执行过程如下:
scala> val myNumFunc= (num) => num * 2
<console>:12: error: missing parameter type
val myNumFunc= (num) => num * 2
^
可以看出,解释器会报错,因为,全部省略以后,实际上,解释器也无法推断出类型,所有,需要你提供类型声明。
下面我们尝试一下,省略num的类型声明,但是,给出myNumFunc的类型声明,在Scala解释器中的执行过程如下:
scala> val myNumFunc: Int=>Int = (num) => num * 2
myNumFunc: Int => Int = <function1>
可以看出,顺利运行通过,不会报错,因为,给出了myNumFunc的类型为“Int=>Int”以后,解释器可以推断出num类型为Int类型。
下面再来看一下什么是“闭包”?
闭包是一个函数,一种比较特殊的函数。为了解释闭包这个概念,我们在Linux系统的“/usr/local/scala/mycode”目录下新建test.scala代码文件,里面包含以下内容:
object MyTest{
def main(args: Array[String]): Unit={
def plusStep(step: Int) = (num: Int) => num + step
//给step赋值
val myFunc = plusStep(3)
//调用myFunc函数
println(myFunc(10))
}
}
然后,在Linux系统的Shell命令提示符下,运行scala命令运行test.scala代码,如下:
scala test.scala
13 //这是运行结果
在上面的MyTest中, step是一个自由变量,它的值只有在运行的时候才能确定,num的类型是确定的,num的值只有在调用的时候才被赋值。这样的函数,被称为“闭包”,它反映了一个从开放到封闭的过程。
如何准确理解“从开放到封闭的过程”呢?下面具体解释一下。
前面说过,闭包是一个函数,一种比较特殊的函数,它和普通的函数有很大区别。对于一个普通的函数而言,比如:
val addMore=(x:Int)=>x>0
上面就定义了一个普通的函数,在函数中只会应用函数中定义的变量,比如x就是函数的传入参数,不会引用函数外部的变量。而闭包则不同,闭包会引用函数外部的变量,下面是一个闭包定义的实例:
val addMore=(x:Int)=>x+more
可以看出,这个函数定义中,引用了变量more,而more并没有在函数中定义,是一个函数外部的变量。如果这个时候去编译执行这条语句,编译器会报错,因为,more是一个自由变量,还没有绑定具体的值,因此,我们说这个时候这个函数是“开放的”。相对而言,这时的x就是一个绑定的变量,已经在函数中给出了明确的定义。因此,为了能够让addMore正常得到结果,不报错,必须在函数外部给出more的值,如下:
scala> var more = 1
more: Int = 1
scala> val addMore = (x: Int) => x + more
addMore: (Int) => Int = < function>
scala> addMore(10)
res19: Int = 11
从上面代码可以看出,我们给more确定具体的值1以后,就让函数addMore中的more变量绑定了具体的值1,不再是“自由变量”,而是被绑定具体的值了,或者说“被关闭”了,这也是为什么我们说这样一个函数叫做“闭包”,它反映了一个从开放到封闭的过程。
而且,闭包(也是一个函数)对捕获变量作出的改变在闭包之外也是可见的,比如,上面的例子中,如果在addMore函数体中对more做了修改,在函数外部也可以看到这种变化。
另外,每次addMore函数被调用时都会创建一个新闭包。每个闭包都会访问闭包创建时活跃的more变量,比如:
scala> var more = 1
more: Int = 1
scala> val addMore = (x: Int) => x + more
addMore: (Int) => Int = < function>
scala> addMore(10)
res19: Int = 11
scala> more =9
more: Int = 9
scala> addMore(10)
res20: Int = 19
从上面可以看出,第1次调用addMore(10)的时候,捕获到的自由变量more的值是1,因此,addMore(10)的结果是11。第2次调用addMore(10)的时候,捕获到的自由变量more的值是9,因此,addMore(10)的结果是19。结论:每个闭包都会访问闭包创建时活跃的more变量。
高阶函数
前面已经说过,函数在Scala中是“头等公民”,它的使用方法和任何其他变量是一样的。一个接受其他函数作为参数或者返回一个函数的函数就是高阶函数。
现在我们给出一个简单的高阶函数实例。假设有一个函数对给定两个数区间中的所有整数求和:
def sumInts(a: Int, b: Int): Int = {
if(a > b) 0 else a + sumInts(a + 1, b)
}
现在,我们重新设计函数sumInts的实现方式,让一个函数作为另一个函数的参数:
//定义了一个新的函数sum,以函数f为参数
def sum(f: Int => Int, a: Int, b: Int): Int ={
if(a > b) 0 else f(a) + sum(f, a+1, b)
}
//定义了一个新的函数self,该函数的输入是一个整数x,然后直接输出x自身
def self(x: Int): Int = x
//重新定义sumInts函数
def sumInts(a: Int, b: Int): Int = sum(self, a, b)
从上面定义可以看出,对于函数sum而言,它的参数类型是(Int=>Int, Int, Int),结果类型是Int,因此,函数sum的类型是:
(Int=>Int, Int, Int) => Int
也就是说,函数sum是一个接受函数参数的函数,因此,是一个高阶函数。
我们上面这种处理方式,看起来“很折腾”,原来很简单的函数sumInts,现在要绕一大圈来实现,这么曲折,到底有什么意义呢?
如果我们单纯只从sumInts的角度来看,上面这种折腾的做法,确实是得不偿失的,但是,如果存在下面这些应用场景,你就可以发现,这种折腾的做法是相当值得的。
比如,现在需要求出连续整数的平方和,代码如下:
def square(x: Int): Int = x * x
def sumSquares(a: Int, b: Int): Int = {
if(a > b) 0 else square(a) + sumSquares(a + 1, b)
}
再比如,现在需要求出连续整数的关于2的幂次和,代码如下:
def powerOfTwo(x: Int): Int = {
if(x == 0) 1 else 2 * powerOfTwo(x-1)
}
def sumPowersOfTwo(a: Int, b: Int): Int = {
if(a > b) 0 else powerOfTwo(a) + sumPowersOfTwo(a+1, b)
}
上面的函数都是从a到b的f(n)的累加形式(其中a<=n<=b),唯一的区别就是各种场景下f(n)的具体实现不同,所以,我们可以抽取这些函数中共同的部分重新编写函数sum,并把定义的f(n)作为一个参数传入到高阶函数sum中,代码如下:
def sum(f: Int => Int, a: Int, b: Int): Int = {
if(a > b) 0 else f(a) + sum(f, a+1, b)
}
def self(x: Int): Int = x
def square(x: Int): Int = x * x
def powerOfTwo(x: Int): Int = if(x == 0) 1 else 2 * powerOfTwo(x-1)
def sumInts(a: Int, b: Int): Int = sum(self, a, b)
def sumSquared(a: Int, b: Int): Int = sum(square, a, b)
def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)
最后,让我们把上述代码放入到Linux系统的“/usr/local/scala/mycode”目录下新建test.scala代码文件进行测试,文件里面包含以下内容:
def sum(f: Int => Int, a: Int, b: Int): Int = {
if(a > b) 0 else f(a) + sum(f, a+1, b)
}
def self(x: Int): Int = x
def square(x: Int): Int = x * x
def powerOfTwo(x: Int): Int = if(x == 0) 1 else 2 * powerOfTwo(x-1)
def sumInts(a: Int, b: Int): Int = sum(self, a, b)
def sumSquared(a: Int, b: Int): Int = sum(square, a, b)
def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)
println(sumInts(1,5))
println(sumSquared(1,5))
println(sumPowersOfTwo(1,5))
然后,在Linux系统的Shell命令提示符下,运行scala命令运行test.scala代码,如下:
scala test.scala
15 //这是运行结果
55
62
从上面的介绍中可以发现,高阶函数实现了强大的功能。表面上都是定义了sum函数,形式都一样,但是,针对不同应用场景,每个sum函数中调用的函数f,都具备了不同的处理逻辑。
占位符语法
为了让函数字面量更加简洁,我们可以使用下划线作为一个或多个参数的占位符,只要每个参数在函数字面量内仅出现一次。
下面是一个实例(我们在Scala解释器中运行,从而可以立即看到运行结果):
scala> val numList = List(-3, -5, 1, 6, 9)
numList: List[Int] = List(-3, -5, 1, 6, 9)
scala> numList.filter(x => x > 0 )
res1: List[Int] = List(1, 6, 9)
scala> numList.filter(_ > 0)
res2: List[Int] = List(1, 6, 9)
从上面运行结果可以看出,下面两个函数字面量是等价的。
x => x>0
_ > 0
当采用下划线的表示方法时,对于列表numList中的每个元素,都会依次传入用来替换下划线,比如,首先传入-3,然后判断-3>0是否成立,如果成立,就把该值放入结果集合,如果不成立,则舍弃,接着再传入-5,然后判断-5>0是否成立,依此类推。