Scala-函数式编程
1.函数式编程
1.1 面向对象和面向过程
面向对象
- 按照功能划分问题,将功能分解成不同的的对象,定义对象中的行为和属性,最终通过对象的行为调用来解决问题。
- 优点:
- 耦合度低。
- 缺点:
- 执行效率低。
面向过程
- 按照步骤解决问题。
- 优点:
- 执行效率高。
- 缺点:
- 耦合度高。
1.2 函数式编程
函数式编程和命令式编程的区别:
- 其实面向对象和面向过程都是命令式编程(计算机的命令),其实语言其实都是面向计算机硬件的抽象。
- int a=1;这里a只是内存地址的引用,还可以让a=其他数,是个变量。
- 命令式编程的编码都可以翻译成计算机底层的指令,对计算机最友好的语言,是计算机的子程序。
- 而函数式编程则重点关注的是数据的映射关系(自变量因变量的映射关系), 不关心计算机底层如何实现,这里的函数指的是数学中的函数。
- val a =1 ,函数式编程中其实没有变量,即一个常量,a=1不能再让a=其他数,这更符合数学中函数的定义。
- sacla中因为其有函数式编程的特点,所以推荐能用常量的不要用变量。
- 定义一个函数,求未知数的值,函数式编程中每一段程序都有一个返回值。
- 函数式编程本质就是数据的映射关系,定义一个表达式,通过该表达式不同的求值,做函数的映射关系。
- 函数至简原则:能省则省 ,只专注于对应的业务代码。
- java1.8中的函数式编程就是参照于此,简化和业务无关的逻辑。
两种编程范式的优缺点:
- 命令式编程:
- 是对于计算机有好的编程,因为都是对应计算机命令的编程。
- 因为最终都是翻译成计算机命令,所以其受计算机环境的影响很大。
- 函数式编程:
- 是对于人更好理解的,定义函数的理念和数学上一致,不用关心计算机底层。
- 定义一个函数,那么其功能就确定了,该公式是不可变的,入参或者说未知数可以来回变,但是同一个值对应的结果一定是唯一的。
- 天然适用于分布式计算:
- 函数式编程中没有变量,都是常量,处理逻辑的过程中具有不可变性,不受外界影响,天然适合在不同的机器上运行,最终只需要将各个机器上的值汇总即可,特别的适合大数据处理的分布式计算。
scala兼容了两种编程范式的特点,scala是一个面向对象的语言,同时也是一个面向编程的语言。
2. 函数基础
2.1 函数基本语法
2.1.1 例子
object TestFunction {
def main(args: Array[String]): Unit = {
// (1)函数定义
def f(arg: String): Unit = {
println(arg)
}
// (2)函数调用
// 函数名(参数)
f("hello world")
}
}
2.2 函数和方法的区别
java中的方法强调的是:
- 类中的函数,类中定义的位置是有限制的。
- 比如就不能在方法中定义方法。
- 方法可以进行重载和重写。
scala中的函数
- 定义在类中的任意代码块中。
- Scala 语言可以在任何的语法结构中声明任何的语法。
- 函数没有重载和重写的概念。
- Scala 中函数可以嵌套定义。
2.2.1 例子
object FunctionAndMethod {
def main(args: Array[String]): Unit = {
// 定义函数
def sayHi(name: String): Unit = {
println("hi, " + name)
}
// 调用函数,根据函数的作用域,按照就近原则,先调用main中定义的sayHi函数
sayHi("alice")
// 调用对象方法,为了调用方法可以如下调用
FunctionAndMethod.sayHi("bob")
// 获取方法返回值
val result = FunctionAndMethod.sayHello("cary")
println(result)
}
// 定义对象的方法
def sayHi(name: String): Unit = {
println("Hi, " + name)
}
def sayHello(name: String): String = {
println("Hello, " + name)
return "Hello"
}
}
2.3 函数的定义
- 函数 1:无参,无返回值
- 函数 2:无参,有返回值
- 函数 3:有参,无返回值
- 函数 4:有参,有返回值
- 函数 5:多参,无返回值
- 函数 6:多参,有返回值
2.3.1 例子
object FunctionDefine {
def main(args: Array[String]): Unit = {
// (1)函数1:无参,无返回值
def f1(): Unit = {
println("1. 无参,无返回值")
}
f1()
println(f1())
println("=========================")
// (2)函数2:无参,有返回值
def f2(): Int = {
println("2. 无参,有返回值")
return 12
}
println(f2())
println("=========================")
// (3)函数3:有参,无返回值
def f3(name: String): Unit = {
println("3:有参,无返回值 " + name)
}
println(f3("alice"))
println("=========================")
// (4)函数4:有参,有返回值
def f4(name: String): String = {
println("4:有参,有返回值 " + name)
return "hi, " + name
}
println(f4("alice"))
println("=========================")
// (5)函数5:多参,无返回值
def f5(name1: String, name2: String): Unit = {
println("5:多参,无返回值")
println(s"${name1}和${name2}都是我的好朋友")
}
f5("alice","bob")
println("=========================")
// (6)函数6:多参,有返回值
def f6(a: Int, b: Int): Int = {
println("6:多参,有返回值")
return a + b
}
println(f6(12, 37))
}
}
2.4 函数参数
- (1)可变参数 (不确定个参数传入)
- (2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
- (3)参数默认值,一般将有默认值的参数放置在参数列表的后面
- (4)带名参数(入参可以选择性的指定某个参数进行传值)
2.4.1 例子
object Test03_FunctionParameter {
def main(args: Array[String]): Unit = {
// (1)可变参数,其实此时入参已经是集合类型了,这里是数组
def f1(str: String*): Unit = {
println(str)
}
f1("alice")
f1("aaa", "bbb", "ccc")
// (2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
def f2(str1: String, str2: String*): Unit = {
println("str1: " + str1 + " str2: " + str2)
}
f2("alice")
f2("aaa", "bbb", "ccc")
// (3)参数默认值,一般将有默认值的参数放置在参数列表的后面
def f3(name: String = "xxx"): Unit = {
println("My school is " + name)
}
f3("school")
f3()
// (4)带名参数
def f4(name: String = "xx", age: Int): Unit = {
println(s"${age}岁的${name}在学习")
}
f4("alice", 20)
f4(age = 23, name = "bob")
f4(age = 21)
}
}
输出
WrappedArray(alice)
WrappedArray(aaa, bbb, ccc)
str1: alice str2: WrappedArray()
str1: aaa str2: WrappedArray(bbb, ccc)
My school is school
My school is xxx
20岁的alice在学习
23岁的bob在学习
21岁的xx在学习
2.5 函数至简原则
函数至简原则:能省则省 ,只专注于对应的业务代码。
2.5.1 至简原则细节
- (1)return 可以省略,Scala 会使用函数体的最后一行代码作为返回值
- (2)如果函数体只有一行代码,可以省略花括号
- (3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
- (4)如果有 return,则不能省略返回值类型,必须指定
- (5)如果函数明确声明 unit,那么即使函数体中使用 return 关键字也不起作用
- (6)Scala 如果期望是无返回值类型,可以省略等号
- (7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
- (8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
- (9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
2.5.2 例子
object Simplify {
def main(args: Array[String]): Unit = {
def f0(name: String): String = {
return name
}
println(f0("xxx"))
println("==========================")
// (1)return可以省略,Scala会使用函数体的最后一行代码作为返回值
def f1(name: String): String = {
name
}
println(f1("xxx"))
println("==========================")
// (2)如果函数体只有一行代码,可以省略花括号
def f2(name: String): String = name
println(f2("xxx"))
println("==========================")
// (3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
def f3(name: String) = name
println(f3("xxx"))
println("==========================")
// (4)如果有return,则不能省略返回值类型,必须指定
// def f4(name: String) = {
// return name
// }
//
// println(f4("xxx"))
println("==========================")
// (5)如果函数明确声明unit,那么即使函数体中使用return关键字也不起作用
def f5(name: String): Unit = {
return name
}
println(f5("xxx"))
println("==========================")
// (6)Scala如果期望是无返回值类型,可以省略等号
def f6(name: String) {
println(name)
}
println(f6("xxx"))
println("==========================")
// (7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
def f7(): Unit = {
println("xxx")
}
f7()
f7
println("==========================")
// (8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
def f8: Unit = {
println("xxx")
}
// f8()
f8
println("==========================")
// (9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
def f9(name: String): Unit = {
println(name)
}
// 匿名函数,lambda表达式
(name: String) => { println(name) }
println("==========================")
def f10 = (x:String)=>{println("wusong")}
def f11(f:String=>Unit) = {
f("")
}
f10(f0)
println(f10((x:String)=>{println("wusong")}))
// 匿名函数的简化原则
f((name: String) => {
println(name)
})
// (1)参数的类型可以省略,会根据形参进行自动的推导
f((name) => {
println(name)
})
// (2)类型省略之后,发现只有一个参数,则圆括号可以省略;其他情况:没有参数和参数超过1的永远不能省略圆括号。
f( name => {
println(name)
})
// (3)匿名函数如果只有一行,则大括号也可以省略
f( name => println(name) )
// (4)如果参数只出现一次,则参数省略且后面参数可以用_代替,参数名只在函数中出现一次
f( println(_) )
// (5) 如果可以推断出,当前传入的println是一个函数体,而不是调用语句,可以直接省略下划线
f( println )
println("=========================")
}
}
3. 函数高级
3.1 高阶函数
1)函数作为值传递
object HighOrderFunction {
def main(args: Array[String]): Unit = {
def f(n: Int): Int = {
println("f调用")
n + 1
}
// 1.普通的函数调用
val result: Int = f(123)
println(result)
// 2. 函数作为值进行传递,有点像重命名
// f1,f2要的是函数体,而不是触发函数
val f1: Int=>Int = f
val f2 = f _
//f1,f2输出的是函数对象,对象引用不同
println(f1)
println(f1(12))
println(f2)
println(f2(35))
// 3 无参函数
def fun(): Int = {
println("fun调用")
1
}
//调用函数体
val f3: ()=>Int = fun
val f4 = fun _
println(f3)
println(f4)
//注意:如果写val f4 = fun 那这就是一次函数
}
}
输出
f调用
124
com.pl.HighOrderFunction$$$Lambda$5/1510067370@19bb089b
f调用
13
com.pl.HighOrderFunction$$$Lambda$6/1908923184@4563e9ab
com.pl.HighOrderFunction$$$Lambda$7/1289479439@7cf10a6f
com.pl.HighOrderFunction$$$Lambda$8/6738746@7e0babb1
2)函数作为参数传递
object Test1 {
def main(args: Array[String]): Unit = {
// (1)定义一个函数,函数参数还是一个函数签名;f 表示函数名称;(Int,Int) 表示输入两个 Int 参数;Int 表示函数返回值
def dualEval(op:(Int,Int)=>Int,a:Int,b:Int):Int={
op(a,b)
}
// (1)定义一个函数,函数参数还是一个函数签名;f 表示函数名称;(Int,Int) 表示输入两个 Int 参数;Int 表示函数返回值
def add(a:Int,b:Int):Int={
a+b;
}
// (3)将 add 函数作为参数传递给 f1 函数,如果能够推断出来不是调用
println(dualEval(add,1,2))
println(dualEval((a,b)=>a+b,1,2))
}
}
3)函数作为函数返回值返回
package chapter05.test
object Test1 {
def main(args: Array[String]): Unit = {
// 3. 函数作为函数的返回值返回
// Int=>Unit 返回函数的入参和返回值
def f5(): Int=>Unit = {
def f6(a: Int): Unit = {
println("f6调用 " + a)
}
f6 // 将函数直接返回
}
val f6 = f5()
println(f6)
println(f6(25))
println(f5()(25))
}
}
输出
chapter05.test.Test1$$$Lambda$1/1989780873@47f37ef1 // 返回函数对象
f6调用 25
()
f6调用 25
()
4) 引用案例
其实高阶函数的一个应用比较多的场合是:定义集合中数据的操作,将操作抽象出来。
object Test07_Practice_CollectionOperation {
def main(args: Array[String]): Unit = {
val arr: Array[Int] = Array(12, 45, 75, 98)
// 对数组进行处理,将操作抽象出来,处理完毕之后的结果返回一个新的数组
def arrayOperation(array: Array[Int], op: Int=>Int): Array[Int] = {
for (elem <- array) yield op(elem)
}
// 定义一个加一操作
def addOne(elem: Int): Int = {
elem + 1
}
// 调用函数,传递函数 这里可以看出arrayOperation只是定义函数处理的大致流程,具体的逻辑推迟到调用方,和map的逻辑很符合
val newArray: Array[Int] = arrayOperation(arr, addOne)
println(newArray.mkString(","))
// 传入匿名函数,实现元素翻倍
val newArray2 = arrayOperation(arr, _ * 2)
println(newArray2.mkString(", "))
}
}
5)扩展练习
(1) 定义一个匿名函数,并将它作为值赋给变量 fun。函数有三个参数,类型分别为 Int,String,Char,返回值类型为 Boolean。 要求调用函数 fun(0, “”, ‘0’)得到返回值为 false,其它情况均返回 true。
object Test08_Practice {
def main(args: Array[String]): Unit = {
// 1. 练习1
val fun = (i: Int, s: String, c: Char) => {
if (i == 0 && s == "" && c == '0') false else true
}
println(fun(0, "", '0'))
println(fun(0, "", '1'))
println(fun(23, "", '0'))
println(fun(0, "hello", '0'))
println("===========================")
}
}
(2) 定义一个函数 func,它接收一个 Int 类型的参数,返回一个函数(记作 f1)。 它返回的函数 f1,接收一个 String 类型的参数,同样返回一个函数(记作 f2)。函数 f2 接 收一个 Char 类型的参数,返回一个 Boolean 的值。 要求调用函数 func(0) (“”) (‘0’)得到返回值为 false,其它情况均返回 true。
object Test08_Practice {
def main(args: Array[String]): Unit = {
// 2. 练习2
def func(i: Int): String=>(Char=>Boolean) = {
def f1(s: String): Char=>Boolean = {
def f2(c: Char): Boolean = {
if (i == 0 && s == "" && c == '0') false else true
}
f2
}
f1
}
println(func(0)("")('0'))
println(func(0)("")('1'))
println(func(23)("")('0'))
println(func(0)("hello")('0'))
// 匿名函数简写
def funcc(i: Int): String=>(Char=>Boolean) = {
//匿名函数首先不需要知道名字,且返回值不用写即所有参数类型的定义省略
// def f1(s: String): Char=>Boolean = {
(s: String) =>{
(c: Char)=> {
if (i == 0 && s == "" && c == '0') false else true
}
}
}
//当然还可以进一步缩写
//如果在外侧已经将形参类型定义好,那么内层的匿名函数形参也都是可以确定的
def func1(i: Int): String=>(Char=>Boolean) = {
s => c => if (i == 0 && s == "" && c == '0') false else true
}
println(func1(0)("")('0'))
println(func1(0)("")('1'))
println(func1(23)("")('0'))
println(func1(0)("hello")('0'))
// 上面的简写还可以进一步省略,将 String=>(Char=>Boolean)省略
// 函数的柯里化
def func2(i: Int)(s: String)(c: Char): Boolean = {
if (i == 0 && s == "" && c == '0') false else true
}
println(func2(0)("")('0'))
println(func2(0)("")('1'))
println(func2(23)("")('0'))
println(func2(0)("hello")('0'))
}
}
3.2 闭包&柯里化
闭包
- 如果一个函数,访问到了它的外部(局部)变量的值,那么这个函数和他所处的 环境,称为闭包。
- 即内部函数将依赖的外部变量保存在本函数中,延长了外部函数局部变量的生命周期。
- scala中调用函数相当于创建了一个对象实例,对象实例在heap中,改对象实例打包保存了该对象的环境(外部环境和局部变量),所以不会因为方法弹栈而丢失方法局部变量。
- 比如上面例子中的func2(0)("")('0'),实际上的调用顺序是:func2>f1>f2,f2并不会因为前两者的弹栈而丢失其依赖的局部变量。
函数柯里化
- 把一个参数列表的多个参数变成多个参数列表。
- 一般纯函数式编程就是定义自变量和因变量之间的关系,只有一个入参,得到一个因变量,不存在输入多个自变量得出一个因变量的用法。
- 但是scala中因为需要兼容java和函数式编程,所以没有只有一个入参的限制,可以允许多个入参。
- 函数柯里化可以实现这么一个效果,每一层调用只有一个入参,所以一个参数列表多个参数实际可以变成多个参数列表。
object Test09_ClosureAndCurrying {
def main(args: Array[String]): Unit = {
def add(a: Int, b: Int): Int = {
a + b
}
// 1. 考虑固定一个加数的场景
def addByFour(b: Int): Int = {
4 + b
}
// 2. 扩展固定加数改变的情况
def addByFive(b: Int): Int = {
5 + b
}
// 3. 将固定加数作为另一个参数传入,但是是作为”第一层参数“传入
def addByFour1(): Int=>Int = {
val a = 4
def addB(b: Int): Int = {
a + b
}
addB
}
def addByA(a: Int): Int=>Int = {
def addB(b: Int): Int = {
a + b
}
addB
}
println(addByA(35)(24))
println(addByA(35))
val addByFour2 = addByA(4)
val addByFive2 = addByA(5)
println(addByFour2(13))
println(addByFive2(25))
// 4. lambda表达式简写
def addByA1(a: Int): Int=>Int = {
//def 函数名 返回值均省略
(b: Int) => {
a + b
}
}
//进一步简写 省略形参
def addByA2(a: Int): Int=>Int = {
//def 函数名 形参 返回值均省略
b => a + b
}
// 进一步简写 参数只出现一次,函数只有一行
def addByA3(a: Int): Int=>Int = a + _
val addByFour3 = addByA3(4)
val addByFive3 = addByA3(5)
println(addByFour3(13))
println(addByFive3(25))
// 5. 柯里化 该函数分为两层,几个参数列表几层 一旦用到柯里化,那么其底层必然是闭包
def addCurrying(a: Int)(b: Int): Int = {
a + b
}
println(addCurrying(35)(24))
}
}
输出
59
chapter05.Test09_ClosureAndCurrying$$$Lambda$5/1510067370@2ff4f00f //println(addByA(35))实际输出的是函数对象实例
17
30
17
30
59
3.3 递归
object Test10_Recursion {
def main(args: Array[String]): Unit = {
println(fact(5))
println(tailFact(5))
}
// 阶乘
// 递归算法
// 1) 方法调用自身
// 2) 方法必须要有跳出的逻辑
// 3) 方法调用自身时,传递的参数应该有规律
// 4) scala 中的递归必须声明函数返回值类型
// 递归实现计算阶乘
def fact(n: Int): Int = {
if (n == 0) return 1
//最后一层代码可以省略 return
fact(n - 1) * n
}
//上面这种递归方式有一个很大的确定就是:每层递归都需要产生新的栈帧,如果层数很多,会导致当前的栈中需要保存的栈帧非常多,甚至会出现stack over flow
//递归是以耗费栈空间资源为代价的
//函数式编程语言中提供了一种优化方式:每层栈帧覆盖之前的栈帧,只消耗一个栈帧
// 尾递归实现
def tailFact(n: Int): Int = {
//尾递归 保存每次该层的结果currRes,将每层的结果值不停的往下传,这样每次调用就不需要保存上一层的调用信息了
// @tailrec 可以确保写出的是一个尾递归,如果不是会报错
@tailrec
def loop(n: Int, currRes: Int): Int = {
if (n == 0) return currRes
loop(n - 1, currRes * n)
}
//从1开始算阶乘
loop(n, 1)
}
}
3.4 控制抽象
控制抽象
- 强调的是参数的调用方式
- 传值调用:将一个确定的值当做参数传递。
- 传名调用:将代码块当做参数传递。
object Test11_ControlAbstraction {
def main(args: Array[String]): Unit = {
// 1. 传值参数
def f0(a: Int): Unit = {
println("a: " + a)
println("a: " + a)
}
f0(23)
def f1(): Int = {
println("f1调用")
12
}
f0(f1())
println("========================")
// 2. 传名参数,传递的不再是具体的值,而是代码块
// a: =>Int 该入参可以是代码块,返回值是int
def f2(a: =>Int): Unit = {
//如果a是代码块,那么a每出现一次就执行一遍对应的代码块
println("a: " + a)
println("a: " + a)
}
f2(23)
f2(f1())
f2({
println("这是一个代码块")
29
})
}
}
控制抽象传名参数的特性丰富了scala的功能,比如利用该特性实现自定义关键字。
object Test12_MyWhile {
def main(args: Array[String]): Unit = {
var n = 10
// 1. 常规的while循环
while (n >= 1){
println(n)
n -= 1
}
// 2. 用闭包实现一个函数,将代码块作为参数传入,递归调用 入参 返回值 都是代码块
def myWhile(condition: =>Boolean): (=>Unit)=>Unit = {
// 内层函数需要递归调用,参数就是循环体
def doLoop(op: =>Unit): Unit = {
if (condition){
op
myWhile(condition)(op)
}
}
doLoop _
}
println("=================")
n = 10
myWhile(n >= 1){
println(n)
n -= 1
}
// 3. 用匿名函数实现
def myWhile2(condition: =>Boolean): (=>Unit)=>Unit = {
// 内层函数需要递归调用,参数就是循环体
op => {
if (condition){
op
myWhile2(condition)(op)
}
}
}
println("=================")
n = 10
myWhile2(n >= 1){
println(n)
n -= 1
}
// 3. 用柯里化实现
def myWhile3(condition: =>Boolean)(op: =>Unit): Unit = {
if (condition){
op
myWhile3(condition)(op)
}
}
println("=================")
n = 10
myWhile3(n >= 1){
println(n)
n -= 1
}
}
}
3.5惰性加载
惰性加载
- 当函数返回值被声明为 lazy 时,函数的执行将被推迟,直到我们首次对此取值,该函 数才会执行。
- 即推迟函数的执行时期,只有第一次需要调用的时候才触发其逻辑。
object Test13_Lazy {
def main(args: Array[String]): Unit = {
//惰性加载和控制抽象差不多,只是控制抽象将代码块给入参,而惰性加载将代码块给val
lazy val result: Int = sum(13, 47)
println("1. 函数调用")
println("2. result = " + result)
println("4. result = " + result)
}
def sum(a: Int, b: Int): Int = {
println("3. sum调用")
a + b
}
}
输出
1. 函数调用
3. sum调用 //sum函数第一次调用
2. result = 60
4. result = 60 //sum函数只调用一次将值传给了常量result,后续不会再调用该函数