Scala函数式编程
一.Scala语言特性
1)面向对象编程
- 解决问题时,分解对象、行为、属性,然后通过对象的关系以及行为的调用来解决问题。
例如:
对象:用户
行为:登录、连接JDBC、读取数据库
属性:用户名、密码
Scala语言是一个完全面向对象编程语言:万物皆对象
- 对象的本质:对数据和行为的一个封装
2)函数式编程
- 解决问题时,将问题分解成一个一个的步骤,将每个步骤进行封装(函数),通过调用这些封装好的步骤,解决问题。
例如:
步骤:请求->用户名、密码->连接JDBC->读取数据库
Scala语言是一个完全函数式编程语言:万物皆函数
- 函数的本质:函数可以当做一个值进行传递
3)在Scala中函数式编程和面向对象编程完美融合在一起了
二.Scala函数基础
1.函数和方法的区别
两者区别:
- 1)函数:完成某个特殊功能的代码块
- 2)函数和方法定义的位置不一
- 函数:定义在方法或者函数内部
- 方法:定义在类下的函数,叫方法
- 3)函数没有重载和重写的概念、而方法可以进行重载和重写
- 4)Scala中函数可以嵌套定义,且可以在任何的语法结构中声明任何的语法
案例:
object TestFunction {
// (2)方法可以进行重载和重写,程序可以执行
def main(): Unit = {
}
def main(args: Array[String]): Unit = {
// (1)Scala语言可以在任何的语法结构中声明任何的语法
import java.util.Date
new Date()
// (2)函数没有重载和重写的概念,程序报错
def test(): Unit ={
println("无参,无返回值")
}
test()
def test(name:String):Unit={
println()
}
//(3)Scala中函数可以嵌套定义
def test2(): Unit ={
def test3(name:String):Unit={
println("函数可以嵌套定义")
}
}
}
}
2.函数基本语法
def 函数名(参数列表):返回值类型={
函数体
}
函数类型参数*
其中函数的参数类型也可以是函数类型:
- 函数类型:
(参数类型) => 返回值
- 当参数只有一个时,小括号可以省略
def f9 (函数类型参数): Unit = {}
def f9 (f:Function): Unit = {
1)f:Function:如果使用类Function来限定函数类型参数,并不能很好的表示该参数(如参数列表与返回值的信息)
2)应该使用的函数类型参数 f: (参数列表声明) => 返回值
}
//f9有一个函数类型参数 f :其无返回值、参数类型为String
def f9(f: String => Unit) = {
f("zxy")
}
3.函数参数
1)可变参数:加星号 (如 s : String*
)
- 在一个参数列表中,至多只能有一个可变长参数
- 如果一个参数列表中,有多个参数,应该将可变长参数放到最后
- 有输入参数时:输出 WrappedArray(Hello, Scala)
- 无输入参数时:输出 List()
2)参数默认值
- 在声明函数的时候,函数的形参可以赋默认值
age : Int = 30
- 如果参数有默认值,那么在调用函数的时候,有默认值的参数可以不用传参,使用默认值作为参数
- 如果参数有默认值,那么在调用函数的时候,也可以传递参数,会用传递的参数替换默认值作为参数
3)带名参数:消除默认值参数位置引起的歧义
- 调用时指定参数名:
test(age = 18)
object TestFunction {
def main(args: Array[String]): Unit = {
// (1)可变参数
def test( s : String* ): Unit = {
println(s)
}
// 有输入参数:输出 WrappedArray(Hello, Scala)
test("Hello", "Scala")
// 无输入参数:输出List()
test()
/*输出结果:
WrappedArray(Hello, Scala)
List()
*/
// (2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
def test2( name : String, s: String* ): Unit = {
println(name + "," + s)
}
test2("jinlian", "dalang")
// (3)参数默认值
def test3( name : String, age : Int = 30 ): Unit = {
println(s"$name, $age")
}
// 如果参数传递了值,那么会覆盖默认值
test3("jinlian", 20)
// 如果参数有默认值,在调用的时候,可以省略这个参数
test3("dalang")
// 一般情况下,将有默认值的参数放置在参数列表的后面
def test4( sex : String = "男", name : String ): Unit = {
println(s"$name, $sex")
}
// Scala函数中参数传递是,从左到右
test4("wusong") //是把wusong赋给了sex
//(4)带名参数
test4(name="ximenqing")
}
}
4.函数至简原则*
至简原则:能省则省
- 1)return可以省略(一般不要加),Scala会使用函数体的最后一行代码作为返回值
- 2)如果函数体只有一行代码,可以省略花括号
- 3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
- 4)如果有return,则不能省略返回值类型,必须指定
- 5)如果函数明确声明unit,那么即使函数体中使用return关键字也不起作用
- 6)Scala如果期望是无返回值类型,可以省略等号
- 将无返回值的函数称之为过程
- 7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
- 8)如果函数没有参数列表,那么定义时小括号可以省略,但调用时小括号必须省略
- 9)如果不关心函数名称,只关心函数的逻辑处理,那么函数名(def)可以省略:使用匿名函数
1)前八类案例
def main(args: Array[String]): Unit = {
// 函数标准写法:
def f( s : String ): String = {
return s + " world!"
}
println(f("Hello"))
//应用至简原则:能省则省
//(1) return可以省略,Scala会使用函数体的最后一行代码作为返回值
def f1( s : String ): String = {
s + " world!"
}
println(f1("Hello"))
//(2)如果函数体只有一行代码,可以省略花括号
def f2(s:String) : String = s + " world!"
//(3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
def f3( s : String ) = s + "world!"
println(f3("Hello3"))
//(4)如果有return,则不能省略返回值类型,必须指定。
def f4() : String = {
return "Hello4"
}
println(f4())
//(5)如果函数明确声明unit,那么即使函数体中使用return关键字也不起作用
def f5() : Unit = {
return "Hello5"
}
println(f5()) //输出:()
//(6)Scala如果期望是无返回值类型,可以省略等号
// 将无返回值的函数称之为过程
def f6() {
"Hello6"
}
println(f6())
//(7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
def f7() = "Hello7"
println(f7())
println(f7)
//(8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
def f8 = "Hello8"
//println(f8())
println(f8)
}
2)匿名函数*
第九类:如果不关心函数名称,只关心函数的处理逻辑,那么函数名(def)可以省略
- 一般用匿名函数来简化操作:
(x:Int) => {函数体}
匿名函数的作用:
- 扩展被传入函数的功能:如案例中
f10()
扩展了f9()
的功能 - 具体扩展的功能是通过该匿名函数传递,灵活性高
匿名函数的简化:
- 1)参数的类型可以省略,会根据形参进行自动的推导
- 2)类型省略之后,发现只有一个参数,则圆括号可以省略;
- 其他情况:没有参数和参数超过1的永远不能省略圆括号
- 3)匿名函数如果只有一行,则大括号也可以省略
- 4)如果参数只出现一次,则参数省略且后面参数可以用_代替
- 当有多个参数被_替换时,处理逻辑中参数的顺序不能改变
注意:如果匿名函数输入和输出相同,那么不能简化
-
例如:spark算子中的flatmap
val rdd = sc.makeRDD(List(List(1, 2), List(3, 4), List(5, 6), List(7)), 2) //以下两个变量方式都是通过.var自动生成的,可以看出第二种并没有很好的识别出传入参数的类型,也就不能生成正确的RDD val newRdd: RDD[Int] = rdd.flatMap(list => list) val newRdd: (List[Int] => TraversableOnce[U_]) => RDD[Nothing] = rdd.flatMap(_) // 3.3 打印修改后的RDD中数据 newRdd.collect().foreach(println)
案例1:传递的函数有1个参数
object Test_Function {
def main(args: Array[String]): Unit = {
//函数标准写法:
//f9有一个函数式参数 f :其无返回值、参数类型为String
def f9(f:(String)=>Unit) = {
f("zxy")
}
def f10(s:String) : Unit = {
println(s)
}
//f9中:调用f10
f9(f10)
//开始简化:
//f9(f10)其中函数式参数f10,可以使用匿名函数表示:
//(0)使用匿名函数替代f10 :(x:Int)=>{函数体}
f9((s:String) => {println(s)})
//(1)参数的类型可以省略,会根据f9的形参定义进行自动的推导
f9((s) => {println(s)})
//(2)类型省略之后,发现只有一个参数,则圆括号可以省略;
// 其他情况:没有参数和参数超过1的永远不能省略圆括号。
f9(s => {println(s)})
//(3)匿名函数如果只有一行,则大括号也可以省略
f9(s => println(s))
//(4)如果参数只出现一次,则参数省略且后面参数可以用_代替
f9(println(_))
//以上函数调用都输出:
//zxy
}
}
案例2:传递的函数有两个参数
object Test_Function {
def main(args: Array[String]): Unit = {
// 当有多个参数被_替换时,处理逻辑中参数的顺序不能被识别
def f11(fun : (Int, Int) => Int) : Int = {
fun(1,2)
}
println(f11((x:Int,y:Int) => {x + y}))
//参数类型可以根据实参推到出来
println(f11((x,y) => {x + y}))
//只有一行代码:
println(f11((x,y) => x + y))
//参数只出现一次:但x\y的顺序不能被改变
println(f11(_ + _))
}
}
三.Scala函数高级
1.函数的高阶用法
在Scala语言中,函数是一等公民,其有很多高级的用法:
1)函数可以作为值进行传递
- 语法:在被调用函数后面加上
空格 + _
,相当于把函数当成一个整体,传递给变量fun - 如果明确了变量的数据类型,那么下划线可以省略
var fun = 函数名 _
var fun : (参数列表) => 返回值类型 = 函数名
案例:
object TestFuction {
def main(args: Array[String]): Unit = {
//(1)调用foo函数,把返回值给变量f
val f = foo //调用 foo(),并把Int返回值赋给变量 f
println(f)
//(2)在被调用函数foo后面加上 _,相当于把函数foo当成一个整体,传递给变量f1
val f1 = foo _
foo //调用函数foo
// f1 此时f1代表的是一个函数类型变量,并不是调用该函数。可以输出一下:
println(f1) //输出结果: <function0>
f1() //调用该函数则必须加()
//(3)如果明确变量类型,那么不使用下划线也可以将函数作为整体传递给变量
var f2:()=>Int = foo
//var f2:()=>Int = foo() 报错:是将foo()的执行结果Int数据赋值给f2
//f2不加括号是函数类型变量,其可以直接赋值给另外一个变量(f1也是)
var f3 = f2
f3()
}
def foo():Int = {
println("foo...")
1
}
}
2)函数可以作为参数进行传递
函数可以作为参数进行传递,通过匿名函数
- 扩展函数的功能
- 提高函数的灵活度
案例:
//函数可以作为参数,进行传递(大多数情况都是通过匿名函数的形式)
//定义一个函数calculator,该函数接收一个函数类型的参数op
// op定义了对两个整数要进行什么样的操作
def calculator(a:Int,b:Int,op:(Int,Int)=>Int): Int ={
op(a,b)
}
//函数类型参数传递方式1:定义一个函数f1,完成两个整数的加法运算
def op(a:Int,b:Int):Int={
a + b
}
println(calculator(10,20,op))
//函数类型参数传递方式2:使用匿名函数作为参数直接调用
println(calculator(50, 20, (a: Int, b: Int) => {
a - b
}))
//函数类型参数传递方式3:对匿名函数进行简化
println(calculator(50, 20, _ - _))
println(calculator(50, 20, _ + _))
3)函数可以作为返回值进行返回
- 场景:函数的嵌套使用
- 函数链式调用,通过参数传递数据,在执行的过程中,函数始终占据栈内存,容易导致内存溢出
- 这种情形Scala通过闭包解决,相应的还有柯里化
案例:
object ClosePackage_Function {
def main(args: Array[String]): Unit = {
def f1(): (Int) => Int = {
var a = 10
def f2(b: Int): Int = {
a + b
}
// 返回一个函数类型变量
f2 _
}
//执行f1函数,返回f2,将返回的f2赋佰给ff变量
val fun : (Int) => Int = f1()
//调用ff函数,其实就是调用f2
println(fun(20))
//也可以直接通过如下方式调用
println(f1()(30))
}
}
扩展:java中函数链式调用,缺点:
- m1 -> m2 -> m3 -> m4:容易断链
- 方法执行时会放在栈中,如果在大数据环境下,每个程序都会执行多个嵌套调用的函数,此时栈就会被海量的栈帧占用,耗费内存资源,极容易导-致栈溢出
2.闭包&柯里化
闭包:函数式编程的标配
什么是闭包:
上一节的案例中:
- 内层函数 f2 要访向外层函数 f1 局部变量 a,当外层函数 f1 执行结束之后,f1 会释放栈内存(如果在java中f1的局部变量a也会被释放,f2就不能再调用a);
- 但是scala会自动的延长 f1 函数的局部变量的生命周期,和内层函数 f2 形成一个闭合的效果,我们将这种闭合的效果称之为闭包
1)闭包:如果一个内层函数访问它的外部(局部)变量的值,会自动延长外层函数局部变量的生命周期,外层函数的局部变量与内层函数形成一个闭合的效果,这个函数和他所处的环境,称为闭包
- 闭包 = 外层函数的局部变量 + 内层函数
- 如果存在闭包,那么编译器会生成包含$anonfun$的字节码文件
首先以一个简单的例子开始:
class ClosureDemo {
def func() = {
var i = 2
val inc: () => Unit = () => i = i + 1
val add: Int => Int = (ii: Int) => ii + i
(inc, add)
}
}
在这个代码中,inc和add引用了func函数中的i变量,由于Scala中函数是头等值,因此inc和add将形成闭包来引用外部的i变量。
编译上述代码我们将得到三个class文件:
ClosureDemo.class
ClosureDemo$$anonfun$1.class
ClosureDemo$$anonfun$2.class
这三个文件分别是ClosureDemo类自身和两个闭包,Scala会为每个闭包生成一个Class文件,如果嵌套过深,可能会出现特别长的类名,从而在Windows上引起一些路径过长的错误。
在Spark源码中的ClosureCleaner类中,我们可以看到这样的代码,用来判断这个类是不是闭包:
// Check whether a class represents a Scala closure
private def isClosure(cls: Class[_]): Boolean = {
cls.getName.contains("$anonfun$")
2)函数柯里化:
- 函数柯里化是将一个函数的一个参数列表中的多个参数,拆分为多个参数列表,简化闭包代码的编写
- 函数柯里化,其实就是将复杂的参数逻辑变得简单化
- 函数柯里化一定存在闭包
//原函数
def f1(): (Int)=>Int ={
var a:Int = 10
def f2(b:Int): Int ={
a + b
}
f2 _
}
//f2()函数可以转换为以下函数:返回值可以通过最后一行值推导出来
def f3() ={
var a:Int = 10
//需要指定参数类型,如果使用 _ + b 匿名函数的推导的参数类型为Any
(b:Int) => a + b
}
println(f3()(30))
//函数柯里化,其实就是将复杂的参数逻辑变得简单化,函数柯里化一定存在闭包
//将一个函数的一个参数列表中的多个参数,拆分为多个参数列表,简化闭包代码的编写
def f4()(b:Int):Int = {
var a:Int = 10
a + b
}
println(f4()(20))
//f4在执行的时候,其实会转换为以下结构
def f4() ={
var a:Int = 10
(b:Int) => a + b
}
柯里化,是对函数嵌套的简化,但是这里只有一层函数体, 如果每层函数完成的业务逻辑不一样,一个函数体如何处理?
3.递归
一个函数/方法在函数/方法体内又调用了本身,我们称之为递归调用。
递归算法:
- 方法调用自身
- 方法必须要有跳出的逻辑
- 方法调用自身时,传递的参数应该有规律
- scala中的递归必须声明函数返回值类型
案例:计算阶乘
object jiecheng {
def main(args: Array[String]): Unit = {
println(test(5))
}
def test(i : Int) : Int = {
if (i == 1) {
1
} else {
i * test(i - 1)
}
}
}
4.抽象控制
1)值调用
- 把计算后的值传递过去
object TestControl {
def main(args: Array[String]): Unit = {
def f() : Int = {
println("f...")
10
}
//此时调用f(),并将返回值10传递给foo()
foo(f())
}
def foo(a: Int):Unit = {
println(a)
println(a)
}
}
输出结果:
f...
10
10
2)名调用:代码块
- 把代码块传递过去
- 代码块形参表示类型:
=> 返回类型
object TestControl {
def main(args: Array[String]): Unit = {
def f(): Int = {
println("f...")
10
}
//此时传递f()的代码块部分给foo,其在foo中调用了两次
foo(f())
}
def foo(a: =>Int):Unit = {//注意这里变量a没有小括号了
println(a)
println(a)
}
}
输出结果:
f...
10
f...
10
案例:自定义实现while循环
使用柯里化+名调用+递归实现:
mywhile (循环条件判断代码)(循环体代码) {递归调用方法体}
object Scala11_TestMyWhile {
def main(args: Array[String]): Unit = {
//while循环语句
var n0 = 10
while (n0 >= 1){
println(n0)
n0 -= 1
}
//1.使用柯里化+名调用+递归实现 定义: mywhile
def mywhile(condition: =>Boolean)(op: =>Unit):Unit = {
if(condition){
op
mywhile(condition)(op)
}
}
// 调用 mywhile
var n1 = 10
mywhile(n>=1){
println("mywhile-->" + n1)
n1 -= 1
}
//2.直接使用普通的函数,实现while循环
//柯里化好处1:将一个参数列表的多个参数,拆分为多个参数列表,这样参数所表示的含义,清晰、明确
def mywhile(condition: =>Boolean,op: =>Unit): Unit ={
if(condition){
op
mywhile(condition,op)
}
}
var n2 = 10
mywhile(n >=1,{
println("mywhile-->" + n2)
n2 -= 1
})
//3.使用闭包的形式,实现mywhile循环
//外层函数的参数表示循环条件
//柯里化好处2 :简化闭包的编写
def mywhile(condition: =>Boolean): (=>Unit)=>Unit ={
//内层函数参数表示循环体
def ff(op: =>Unit): Unit ={
if(condition){
op
mywhile(condition)(op)
}
}
ff _
}
var n3 = 10
mywhile(n3 >= 1){
println("mywhile-->" + n3)
n3 -= 1
}
}
}
柯里化的好处:
- 1)将一个参数列表的多个参数,拆分为多个参数列表,这样参数所表示的含义,清晰、明确
- 2)简化闭包的编写
5.惰性函数
当函数返回值被声明为lazy时,函数的执行将被推迟,直到我们首次用到,该函数才会执行。这种函数我们称之为惰性函数。
案例:
def main(args: Array[String]): Unit = {
lazy val res = sum(10, 30)
println("----------------")
println("res=" + res)
}
def sum(n1: Int, n2: Int): Int = {
println("sum被执行...")
return n1 + n2
}
输出结果:
----------------
sum被执行...
res=40
注意:lazy不能修饰var类型的变量