Scala学习笔记-核心特性(1)-函数式编程

函数式编程简介

不同范式的对比:

  • 面向过程:按照步骤按照顺序解决问题
  • 面向对象:分解对象、行为、属性,然后通过对象关系以及行为调用来解决问题。耦合低,复用性高,可维护性强
  • 函数式编程:面向对象和面向过程都是命令式编程,但是函数式编程并不关心具体的运行过程,更加关心数据之间的映射关系。纯粹的函数式编程语言中没有变量的概念,所有的量都是常量。整个执行过程就是在不停的进行表达式的求值,每一段程序都有返回值。(易于编程人员理解,相对地编译器处理起来比较复杂)

函数式编程的编程效率更高,并且由于函数式编程的不可变性,对于函数来说,输入固定则输出固定,与环境上下文等无关。利于并行处理,所以特别适用于大数据处理领域。

Scala将函数式编程和面向对象编程融合在一起。

函数基本语法

基本语法

在Scala中,函数定义如下:

1
2
3
4
5
6
7
def func(arg1: Type1, arg2:Type2, ...): ReturnType = {
...
}
// 举例
def sum(x: Int, y: Int): Int = {
x + y
}

其中包括函数名称、函数参数和对应的类型、函数返回类型以及函数体。

函数和方法的区别:

  • 完成某一功能的程序语句的集合称为函数
  • 类中的函数称为方法
  • 函数并没有重载和重写的概念,但是方法有

注意事项:

  1. Scala语言可以在任何的语法结构中声明任何的语法
  2. Scala中函数可以嵌套定义,在Java中方法中不能再写方法
  3. 函数式编程语言中,函数是一等公民,可以像对象一样赋值,作为参数返回值等,可以在任何代码块中定义函数

函数参数

函数参数的情形:有参、无参、有返回值、无返回值(Unit)

可变参数的写法:

1
2
3
def test(strs: String*): Unit = {
...
}
  • 如果参数列表存在多个参数,则将可变参数放置在最后

在Java中,如果一个方法的参数是可变参数,那么传入一个数组对象也是能够接收的,不过在Scala中,是不能将数组对象传入可变参数,需要而是需要增加:_*进行解析,例如我们希望能够使用可变参数:

1
2
3
def main(args: Array[String]): Unit = {
test(args:_*)
}

默认参数具名参数

1
2
3
4
5
def test(a :Int ,b :Int = 1): Int = {
a + b
}
// 调用
test(a = 1, b = 2)
  • 一般情况下,将有默认值的参数放置在参数列表的后面(不满足顺序也不会报错)
  • 在Scala中有默认参数以及具名参数,但是在Java中没有类似的用法

函数至简原则

  1. return可以省略、Scala会使用函数体的最后一行代码作为返回值
  2. 如果函数体只有一行代码,可以省略花括号
  3. 如果返回值类型能够推断出来,则可以省略
  4. 如果有return,则不能省略返回值类型,必须指定
  5. 如果函数中明确声明了返回Unit,那么即使函数体中使用了return关键字也不起作用
  6. Scala如果期望是无返回值类型,可以省略等号(废弃)
  7. 无参函数声明可以不加括号,调用也可以不加(废弃)
  8. 不关心函数名称的时候,函数名称和def也可以省略

高阶函数

在Scala中,函数是一等公民,我们可以定义函数调用函数、将函数作为值传递、将函数作为参数进行传递、将函数作为返回值

  • 注意这里函数作为参数,接收的类型该如何书写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 定义函数
def test1(): Unit = {
println("调用函数test1")
}
// 调用函数
test1()

// 调用函数得到返回值并打印
val f: Unit = test1()
println(f)
// 将函数整体作为值进行传递(得到一个Lambda函数对象)
// 需要加上下划线_,相当于把函数test1当成一个整体传入
val f1 = test1 _
println(f1)
// 如果明确变量类型,那么不使用下划线也可以
val f2: () => Unit = test1
println(f2)

// 函数作为参数传递
def func1(f: (Int, Int) => Int): Int = {
f(2, 4)
}

def add(a: Int, b: Int): Int = {
a + b
}
// func1需要传入一个函数作为参数,add是我们传进去的作为参数的函数
// 可以传入add _表示函数对象,但是由于类型确定,可以推断出这不是函数调用,因此可以不用冗余的_
println(func1(add _))
println(func1(add))

// 函数作为函数返回值返回
def func2() = {
def func3(): Unit = {
println("调用func3")
}

func3 _
}
// 调用func2()得到返回值还是一个函数
val f3 = func2()
// f3是一个函数,还能够进行调用
println(f3)
f3()

匿名函数

没有名称的函数就是匿名函数,也称为Lambda表达式,格式如下:

1
(参数列表) => {函数体}
  • 匿名函数定义的时候不能有函数的返回值类型
  • 参数列表中的类型可以省略,会根据进行自动的类型推导
  • 类型省略之后发现只有一个参数,则圆括号可以省略
  • 匿名函数如果只有一行,则大括号也可以省略
  • 如果参数在函数体中只出现一次,则参数省略,且后面的参数可以使用_进行代替
  • _下划线必须按照顺序进行接收,使用下划线的时候必须省略参数列表和箭头
1
2
3
4
5
6
7
8
9
10
11
def myOP(op: (Int, Int) => Int): Int = {
println(op(1, 2))
op(1, 2)
}

println("The Return Of myOp Is " + myOP((x, y) => {
x + y
}))
println("The Return Of myOp Is " + myOP({
_ + _
}))

闭包和函数柯里化

闭包:如果一个函数,访问到了它的外部的局部变量的值,那么这个函数和它所处的环境包括那个变量称为闭包。

观察如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// f1的返回值是一个函数
def f1() = {
var a: Int = 10

def f2(b: Int): Int = {
a + b
}

f2 _
}

val f = f1()
println(f(5))

函数f1的返回值是一个函数,通过f接收这个返回值后可以进行函数调用,因此最后得到的结果是5+10=15。但是按照常规分析,这里是存在问题的。上面存在函数的嵌套定义,但是并不存在函数的嵌套调用。我们在调用f1()的时候,并没有执行f2,只是得到了f2作为一个函数的返回值,因此此时f1的栈帧应该已经弹出了,后面再调用f2的时候理应无法访问到f1的局部变量,但是这里确确实实访问到了,这就是闭包的效果。

在Scala中,一切皆对象,因此一个函数实际上也是对象。我们调用一个函数,实际上在JVM的堆内存上有一个函数对象,里面存放了局部变量。后续调用f2的时候,虽然栈帧弹出了,但是堆上的对象还是存在的,因此能够访问到。这里堆上的函数对象,清除的工作依赖于GC机制。

与闭包概念一起出现的还有函数柯里化的概念。

函数柯里化(Currying):指的是将一个参数列表的多个参数,变成多个参数列表的过程。也就是将普通的多参数函数变成高阶函数的过程。例如,上面的函数f1也可以进行柯里化,写成下面的样子,之后就可以连续调用。这样的写法更符合阅读习惯。

1
2
3
4
5
6
7
def f1()(b: Int): Int = {
var a: Int = 10
a + b
}

val f = f1()(5)
println(f)

函数递归

递归:函数调用自身

  • 函数调用自身
  • 必须要有跳出的逻辑
  • scala中的递归必须声明函数返回值类型

在递归过程中,往往会导致栈中空间被大量占用。但是在一种情况下,Scala中可以对栈的占用进行优化,即尾递归的情形。

在传统的递归中,典型的模型是首先执行递归调用,然后获取递归调用的返回值并计算结果。这种方式下,在每次递归调用返回之前,我们并不能得到计算结果。如果一个函数所有的递归形式的调用都出现在函数的末尾,则这种情况称为尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性可以使得在递归过程中没有保存栈帧的必要,可以通过覆盖当前的栈帧而不是在其之上重新添加,因此所使用的栈空间就大大缩减,这使得实际的运行效率更高。

对尾递归的优化依赖于语言,递归可以写成尾递归的形式,在Scala中会对尾递归进行优化,Java中并没有这样的机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正常递归计算阶乘
def factorial(n: Int): Int = {
if (n < 0) return -1
if (n == 0) return 1
return factorial(n - 1) * n
}

// 改进成尾递归的形式
@tailrec
def tailFact(n: Int, res: Int = 1): Int = {
if (n <= 0) return res
else return tailFact(n - 1, res * n)
}

其中的注解tailrec会检查下面的递归函数是否满足尾递归的形式,如果不满足则会报错。在IDEA中也会智能检查尾递归和普通递归,函数旁边会显示不同的标志。

控制抽象

在Scala中存在值调用和名调用两种方式

  • 值调用:按值传递参数,计算值之后再进行传递
  • 名调用:按名称传递参数,直接使用实参替换函数中使用形参的地方,相当于直接在预处理的时候替换,可以传递代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 值传递
def f0(a: Int) = {
println("f0被调用")
println("a = " + a)
}

f0(10)

// 名传递
def f1(a: => Int) = {
println("f1被调用")
println("a=" + a)
println("a=" + a)
}

def f2(): Int = {
println("f2被调用")
2
}

f1(10)
f1(f2())
f1({
println("代码块调用")
5
})

对应的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f0被调用
a = 10
f1被调用
a=10
a=10
f1被调用
f2被调用
a=2
f2被调用
a=2
f1被调用
代码块调用
a=5
代码块调用
a=5

注意这里名调用中参数类型的指定,需要使用=>,并且需要指定代码块或者参数的返回值

我们可以利用名传递来实现一个while关键字的功能,见后面的应用举例

惰性加载

当函数返回值被声明为lazy的时候,函数的执行将被推迟,直到我们首次使用这个值,该函数才会被执行。这种函数被称为惰性函数。(注意,lazy不能修饰var类型的变量,只能修饰函数的返回值)

1
2
3
4
5
6
7
8
def sum(n1: Int, n2: Int) = {
println("sum被执行")
n1 + n2
}

lazy val res = sum(10, 30)
println("--------")
println("res = " + res)

运行结果如下:

1
2
3
--------
sum被执行
res = 40

函数应用举例

模拟Map映射、Filter过滤和Reduce聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// map,将列表中的每个值映射为另外一个值
def map(array: Array[Int], op: Int => Int): Array[Int] = {
for (element <- array) yield op(element)
}

// filter, 查看每个值是否能够加入列表
def filter(array: Array[Int], op: Int => Boolean): Array[Int] = {
for (element <- array if op(element)) yield element
}

// reduce,将列表中的所有值进行两两规约
def reduce(array: Array[Int], op: (Int, Int) => Int): Int = {
var res: Int = 0
for (element <- array) res = op(res, element)
res
}

// 调用上面的函数,将列表中所有的值执行+1操作,去除其中的偶数,最后将它们全部相加
val array = Array(1, 2, 3, 4, 5)
println(map(array, (x: Int) => {
x + 1
}).mkString("Array(", ", ", ")"))
println(filter(array, (x: Int) => {
x % 2 == 1
}).mkString("Array(", ", ", ")"))
println(reduce(array, {
_ + _
}))

利用同一个函数实现在整数上的四则运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def intCalculate(a: Int, b: Int, op: (Int, Int) => Int): Int = {
println(op(a, b))
op(a, b)
}
//实现加减乘除
intCalculate(1, 2, (a: Int, b: Int) => {
a + b
})
intCalculate(3, 1, (a, b) => {
a - b
})
intCalculate(4, 2, {
_ * _
})
intCalculate(6, 3, {
_ / _
})

利用名传递实现while关键字的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 内置的while
var n = 10
while (n >= 1) {
print(n + " ")
n -= 1
}

// 自己实现while
def myWhile(condition: => Boolean)(op: => Unit): Unit = {
if (condition) {
op
myWhile(condition)(op)
}
}

n = 10
myWhile(n >= 1) {
print(n + " ")
n -= 1
}

Scala学习笔记-核心特性(1)-函数式编程
http://example.com/2022/04/30/Scala学习笔记-核心特性-1-函数式编程/
作者
EverNorif
发布于
2022年4月30日
许可协议