基础
概念
函数式编程
函数式编程中的函数指的并不是编程语言中的函数(或方法),它指的是数学意义上的函数,即映射关系(如:y = f(x)),就是 y 和 x 的对应关系。
数学上对于函数的定义是这样的:“给定一个数集 A,假设其中的元素为 x。现对 A 中的元素 x 施加对应法则 f,记作 f(x),得到另一数集 B。假设 B 中的元素为 y。”
所以当我们在讨论“函数式”时,我们其实是在说“像数学函数那样,接收一个或多个输入,生成一个或多个结果,并且没有副作用。
函数式编程语言的特性
在了解了函数编程语言的基本概念之后,我们再来看一下函数式编程语言所具备一些特性,理解这些特性将有助于我们更好的理解“什么是函数式编程”。
-
函数是一等公民,它的意思就是函数与其他数据类型一样,可以把它们存在数组里,当做参数传递,赋值给变量,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,也可以对函数进行组合。
-
高阶函数,在函数式编程中,如果函数能满足下面任一要求就可以被称为高阶函数(higher-order function)
-
柯里化,所谓“柯里化” ,就是把一个多参数的函数 f,转换为单参数函数 g,并且这个函数的返回值也是一个函数。
-
副作用,所谓“副作用”,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
在像 C++ 这样的命令式语言中,函数的意义与数学函数完全不同。例如,假设我们有一个 C++ 函数,它接受一个浮点参数并返回一个浮点结果。从表面上看它可能看起来有点像数学函数意义上的映射实数成实数,但是 C++ 函数可以做的不仅仅是返回一个取决于其参数的数字,它还可以读写其他的全局变量,也可将将输出写入屏幕并接收来自用户的输入。但是,在纯函数式语言中,函数只能读取其参数提供给它的内容,并且它对世界产生影响的唯一方式就是通过它返回的值。 -
纯函数,纯函数编程和函数编程的区别在于:是否允许在函数内部执行一些非函数式的操作,同时这些操作是否会暴露给系统中的其他地方?也就是是否存在副作用。如果不存在副作用,或者说可以不用在意这些副作用,那么就将其称为纯粹的函数式编程。
-
引用透明性,函数无论在何处、何时调用,如果使用相同的输入总能持续地得到相同的结果,就具备了函数式的特征。这种不依赖外部变量或“状态”,只依赖输入的参数的特性就被称为引用透明性(referential transparency)。“没有可感知的副作用”(比如不改变对调用者可见的变量,进行I/O,不抛出异常等)的这些限制都隐含着引用透明性
-
递归和迭代,对于函数式而言,循环体有一个无法避免的副作用,就是它会修改某些对象的状态,通常这些对象又是和其他部分共享的。而且也因为变量值是不可变的,纯函数编程语言也无法实现循环。所以纯函数编程语言通常不包含像 while 和 for 这样的迭代构造器,而是采用的无需修改的递归。
Scala中的函数
- 在scala中,方法和函数几乎可以等同(比如他们的定义、使用、运行机制都一样 的),只是函数的使用方式更加的灵活多样。
- 函数式编程是从编程方式(范式)的角度来谈的,可以这样理解:函数式编程把函 数当做一等公民,充分利用函数、 支持的函数的多种使用方式。 比如: 在Scala当中,函数是一等公民,像变量一样,既可以作为函数的参数使用,也可 以将函数赋值给一个变量. ,函数的创建不用依赖于类或者对象,而在Java当中, 函数的创建则要依赖于类、抽象类或者接口.
- 面向对象编程是以对象为基础的编程方式。
- 在scala中函数式编程和面向对象编程融合在一起了 。
函数的定义
基本语法
def 函数名 ([参数名: 参数类型], ...)[[: 返回值类型] =] {
语句... return 返回值
}
object Fun {
def main(args: Array[String]): Unit = {
var res = sum(10, 100, "-")
println(res)
}
def sum(x: Int, y: Int, oper: String): Int = {
if (oper == "+") {
x + y
} else if (oper == "-") {
x - y
} else if (oper == "*") {
x * y
} else if (oper == "/") {
x / y
} else {
throw new Exception("输入错误")
}
}
}
注意事项
- 函数声明关键字为def (definition)
- [参数名: 参数类型], …:表示函数的输入(就是参数列表), 可以没有。 如果有,多 个参数使用逗号间隔
- 函数中的语句:表示为了实现某一功能代码块
- 函数可以有返回值,也可以没有
- 返回值形式1: : 返回值类型 =
- 返回值形式2: = 表示返回值类型不确定,使用类型推导完成
- 返回值形式3: 表示没有返回值,return 不生效
- 如果没有return ,默认以执行到最后一行的结果作为返回值
递归调用
递归就是一个函数在它的函数体内调用它自身。执行递归函数将反复调用其自身,每调用一次就进入新的一层。递归函数必须有结束条件。
当函数在一直递推,直到遇到墙后返回,这个墙就是结束条件。
所以递归要有两个要素,结束条件与递推关系
重要原则:
- 递归的时候,每次调用一个函数,计算机都会为这个函数分配新的空间,这就是说,当被调函数返回的时候,调用函数中的变量依然会保持原先的值,否则也不可能实现反向输出。
- 函数的局部变量是独立的,不会相互影响
- 递归必须向退出递归的条件逼近,否则就是无限递归,死龟了:)
- 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回 给谁。
案例1: 求出斐波那契数1,1,2,3,5,8,13… 给你一个整数n,求出它的斐波那契数是多少?
def feiBo(inputNum: Int): Int = {
if (inputNum == 1 || inputNum == 2) {
1
} else {
feiBo(inputNum - 1) + feiBo(inputNum - 2)
}
}
案例2:已知 f(1)=3; f(n) = 2*f(n-1)+1; 请使用递归的思想编程,求出 f(n)的值?
def fun01(n: Int): Int = {
if (n == 1) {
3
} else {
2 * fun01(n - 1) + 1
}
}
案例3: 猴子吃桃子问题 有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃 其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有 1个桃子了。问题:最初共多少个桃子?
def monkey(n: Int): Int = {
var res: Int = 0
if (n == 10) {
re s = 1
res
} else {
monkey(n + 1) * 2
}
}
函数注意事项
- 函数的形参列表可以是多个, 如果函数没有形参,调用时 可以不带()
object Test002 {
def main(args: Array[String]): Unit = {
test001(1, 2L)
test002
}
def test001(m: Int, n: Long): Unit = {
println("多个参数")
}
def test002(): Unit = {
println("没有形参")
}
}
- 形参列表和返回值列表的数据类型可以是值类型和引用类型。
object Test003 {
def main(args: Array[String]): Unit = {
val dog = new Dog;
val dog002 = test003(1, dog)
println(dog.name)
println(dog002.name)
}
def test003(n: Int, dog: Dog): Dog = {
dog.name = "金毛"
return dog
}
}
class Dog {
var name: String = "二哈"
}
- Scala中的函数可以根据函数体最后一行代码自行推断函数返回值类型。那么在 这种情况下,return关键字可以省略。
def sum001(x: Int, y: Int): Int = {
x + y
}
- 因为Scala可以自行推断,所以在省略return关键字的场合,返回值类型也可以 省略。
def sum002(x: Int, y: Int) = {
x + y
}
-
如果函数明确使用return关键字,那么函数返回就不能使用自行推断了,这时要明 确写成 : 返回类型 = ,当然如果你什么都不写,表示此函数没有返回值,即使有return 返回值为()。
-
如果函数明确声明无返回值(声明Unit),那么函数体中即使使用return关键字 也不会有返回值。
def sum002(x: Int, y: Int): Unit = {
return x + y // 返回值为()
}
- 如果明确函数无返回值或不确定返回值类型,那么返回值类 型可以省略(或声明为Any)。
def test001(str: String): Any = {
if (str.length >= 3) {
"123" //返回字符串
} else {
3 //返回数字
}
}
- Scala语法中任何的语法结构都可以嵌套其他语法结构(灵活),即:函数中可以 再声明/定义函数,类中可以再声明类 ,方法中可以再声明/定义方法。
object Test005 {
def main(args: Array[String]): Unit = {
def sayHai(): Unit = { //编译后为 sayHai()$1 ,// 在main函数中定义一个函数,编译后此方法默认会加上private final,
println("main 方法中的sayHai")
def sayHai(): Unit = { //编译后为 sayHai()$2
println("main 方法中的sayHai中的sayHai")
}
}
sayHai() // main 方法中的sayHai
}
//
def sayHai(): Unit = {
println("main 方法外的sayHai")
}
}
- Scala函数的形参,在声明参数时,直接赋初始值(默认值),这时调用函数时,如 果没有指定实参,则会使用默认值。如果指定了实参,则实参会覆盖默认值。
object Test006 {
def main(args: Array[String]): Unit = {
sayHai()
sayHai("二哈")
}
def sayHai(name: String = "小白"): Unit = {
println(name)
}
}
- 如果函数存在多个参数,每一个参数都可以设定默认值,那么这个时候,传递的 参数到底是覆盖默认值,还是赋值给没有默认值的参数,就不确定了(默认按照 声明顺序[从左到右])。在这种情况下,可以采用带名参数。
object Test006 {
def main(args: Array[String]): Unit = {
// 没有默认值,此时都会采用默认值
sayHai()
// 此时只有一个值,就不确定到底是哪个值,从左到右替换
sayHai("王白")
// 只想传sex时,使用带名参数
sayHai(sex = "母的")
}
def sayHai(name: String = "小白", sex: String = "男"): Unit = {
println(name + sex)
}
}
-
scala 函数的形参默认是val的,因此不能在函数中进行修改。
-
递归函数未执行之前是无法推断出来结果类型,在使用时必须有明确的返回值 类型。
-
Scala函数支持可变参数。
def test001(args: Int*): Unit = {
//args 是集合, 通过 for循环 可以访问到各个值
for (elem <- args) {
println(elem)
}
}
- 特殊函数。定义一个函数 ,省略了(),{},=
def f1 = 1
println(f1)
过程
将函数的返回类型为Unit的函数称之为过程(procedure),如果明确函数没有返回值, 那么等号可以省略。
注意事项:
- 注意区分: 如果函数声明时没有返回值类型,但是有 = 号,可以进行类型推断最 后一行代码。这时这个函数实际是有返回值的,该函数并不是过程。(这点在讲解 函数细节的时候讲过的.)
- 开发工具的自动代码补全功能,虽然会自动加上Unit,但是考虑到Scala语言的简 单,灵活,最好不加.
惰性函数
惰性计算(尽可能延迟表达式求值)是许多函数式编程语言的特性。惰性集合在需 要时提供其元素,无需预先计算它们,这带来了一些好处。首先,您可以将耗时的 计算推迟到绝对需要的时候。其次,您可以创造无限个集合,只要它们继续收到请 求,就会继续提供元素。函数的惰性使用让您能够得到更高效的代码。Java 并没有 为惰性提供原生支持,可时候用懒汉模式,Scala提供了。
惰性函数:当函数返回值被声明为lazy时,函数的执行将被推迟,直到我们首次对此取值,该函 数才会执行。这种函数我们称之为惰性函数,在Java的某些框架代码中称之为懒加载 (延迟加载)。
注意事项:
- lazy 不能修饰 var 类型的变量
- 不但是 在调用函数时,加了 lazy ,会导致函数的执行被推迟,我们在声明一个变 量时,如果给声明了 lazy ,那么变量值得分配也会推迟。 比如 lazy val i = 10
object Test007 {
def main(args: Array[String]): Unit = {
lazy val res = sum(1, 2)
println("=============")
// res未使用时,不会输出sum
println("res="+res)
}
def sum(x: Int, y: Int): Int = {
println("sum")
x + y
}
}
异常
Scala提供try和catch块来处理异常。try块用于包含可能出错的代码。catch块用于 处理try块中发生的异常。可以根据需要在程序中有任意数量的try…catch块。 语法处理上和Java类似,但是又不尽相同。scala只有运行时异常。
注意事项:
- java语言按照try—catch-catch…—finally的方式来处理异常 ,不管有没有异常捕获,都会执行finally, 因此通常可以在finally代码块中释放资源 。可以有多个catch,分别捕获对应的异常,这时需要把范围小的异常类写在前面, 把范围大的异常类写在后面,否则编译错误。会提示 “Exception ‘java.lang.xxxxxx’ has already been caught”。
- Scala我们将可疑代码封装在try块中。 在try块之后使用了一个catch处理程序来捕获异 常。如果发生任何异常,catch处理程序将处理它,程序将不会异常终止。
- Scala的异常的工作机制和Java一样,但是Scala没有“checked(编译期)”异常,即 Scala没有编译异常这个概念,异常都是在运行的时候捕获处理。
- Scala用throw关键字,抛出一个异常对象。所有异常都是Throwable的子类型。throw 表达式是有类型的,就是Nothing,因为Nothing是所有类型的子类型,所以 throw表达式可以用在需要类型的地方
- 在Scala里,借用了模式匹配的思想来做异常的匹配,因此,在catch的代码里,是 一系列case子句来匹配异常。当匹 配上后 => 有多条语句可以换行写,类似 java 的 switch case x: 代码块…
- Scala异常捕捉的机制与其他语言中一样,如果有异常发生,catch子句是按次序捕捉的。 因此,在catch子句中,越具体的异常越要靠前,越普遍的异常越靠后,如果把越 普遍的异常写在前,把具体的异常写在后,在scala中也不会报错,但这样是非常 不好的编程风格
- Scala中finally子句用于执行不管是正常处理还是有异常发生时都需要执行的步骤,一般 用于对象的清理工作,这点和Java一样。
- Scala提供了throws关键字来声明异常。可以使用方法定义声明异常。 它向调用者 函数提供了此方法可能引发此异常的信息。 它有助于调用函数处理并将该代码包 含在try-catch块中,以避免程序异常终止。在scala中,可以使用throws注释来声 明异常。
object Test008 {
def main(args: Array[String]): Unit = {
try {
var x = 10 / 0
} catch {
case e: ArithmeticException => {
println("ArithmeticException" + e.getMessage)
// ....
}
case e: Exception => println("Exception")
} finally {
println("finally")
}
}
@throws(classOf[ArithmeticException])
def chu(x: Int, y: Int): Int = {
if (y <= 0) {
throw new Exception("除数不能为0")
} else {
x / y
}
}
}
偏函数
将包在大括号内的一组case语句封装为函数,我们称之为偏函数,它只 对会作用于指定类型的参数或指定范围值的参数实施计算,超出范围的 值会忽略(未必会忽略,这取决于你打算怎样处理)。在对符合某个条件,而不是所有情况进行逻辑操作时,使用偏函数是一 个不错的选择。偏函数在Scala中是一个特质PartialFunction。
object Test123 {
def main(args: Array[String]): Unit = {
val list = List(1, 2, 3, 4, "abc")
val addOne = new PartialFunction[Any, Int] {
override def isDefinedAt(x: Any): Boolean = if (x.isInstanceOf[Int]) true else false
override def apply(v1: Any): Int = v1.asInstanceOf[Int] + 1
}
val ints: List[Int] = list.collect(addOne)
println(ints)
// 简化模式
val ints1: List[Int] = list.collect { case i: Int => i + 1 }
println(ints1)
}
}
注意事项:
- 使用构建特质的实现类(使用的方式是PartialFunction的匿名子类)
- PartialFunction 是个特质(看源码)
- 构建偏函数时,参数形式 [Any, Int]是泛型,第一个表示参数类型,第二个表示 返回参数
- 当使用偏函数时,会遍历集合的所有元素,编译器执行流程时先执行isDefinedAt() 如果为true ,就会执行 apply, 构建一个新的Int 对象返回
- 执行isDefinedAt() 为false 就过滤掉这个元素,即不构建新的Int对象.
- map函数不支持偏函数,因为map底层的机制就是所有循环遍历,无法过滤处理 原来集合的元素
- collect函数支持偏函数
作为参数的函数
函数作为一个变量传入到了另一个函数中,那么该作为参数的函数的类型是: function1,即:(参数类型) => 返回类型。
object Test124 {
def main(args: Array[String]): Unit = {
val ints: Array[Int] = Array(1, 2, 3, 4).map(test001(_))
println(ints.mkString(","))
}
def test001(x: Int): Int = {
x + 5
}
}
- map(test001()) 中的test001() 就是将test001这个函数当做一个参数传给了map,_ 这里代表从集合中遍历出来的一个元素。
- test001(_) 这里也可以写成 test001表示对 Array(1,2,3,4) 遍历,将每次遍历的元 素传给plus的 x
- 进行 x+5运算后,返回新的Int ,并加入到新的集合中
- def map[B, That](f: A => B) 的声明中的 f: A => B 一个函数
匿名函数
没有名字的函数就是匿名函数,可以通过函数表达式来设置匿名函数。
val triple = (x: Double) => 3 * x
println (triple(3))
- (x: Double) => 3 * x 就是匿名函数
- (x: Double) 是形参列表, => 是规定语法表示后面是函数体, 3 * x 就是函数 体,如果有多行,可以 {} 换行写.
- triple 是指向匿名函数的变量。
高阶函数
能够接受函数作为参数的函数,叫做高阶函数 (higher-order function)。可使应用 程序更加健壮。
object Test125 {
def main(args: Array[String]): Unit = {
val d: Double = test(sum, 3.0)
println(d)
}
// 普通函数,接受一个参数,返回一个参数
def sum(d: Double): Double = {
d + d
}
// 高阶函数,它可以接收f: Double => Double 函数
def test(f: Double => Double, n1: Double) = {
f(n1)
}
}
参数(类型)推断
参数推断省去类型信息(在某些情况下[需要有应用场景],参数类型是可以推断 出来的,如list=(1,2,3) list.map() map中函数参数类型是可以推断的),同时也 可以进行相应的简写。
object Test126 {
def main(args: Array[String]): Unit = {
val list = List(1, 2, 3, 4)
// map是一个高阶函数,因此也可以直接传入一个匿名函数,完成map
println(list.map((x: Int) => x + 1)) //(2,3,4,5)
// 当传入的函数,只有单个参数时,可以省去括号
println(list.map(x => x + 1))
// 如果变量只在=>右边只出现一次,可以用_来代替
println(list.map(_ + 1))
}
}
注意事项:
- 参数类型是可以推断时,可以省略参数类型
- 当传入的函数,只有单个参数时,可以省去括号
- 如果变量只在=>右边只出现一次,可以用_来代替
闭包(closure)
闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。
闭包通常来讲可以简单的认为是可以访问一个函数里面局部变量的另外一个函数。
def mulCurry(x: Int) = (y: Int) => x * y
println(mulCurry(10)(9))
函数柯里化
函数编程中,接受多个参数的函数都可以转化为接受单个参数的函数,这个转 化过程就叫柯里化。柯里化就是证明了函数只需要一个参数而已。柯里化是面向函数思想的必然产生结果。
def add(x:Int)(y:Int) = x + y
控制抽象
控制抽象被描述为是一系列语句的聚集,是一种特殊的函数,因为它是本质上只是对一系列语句的封装,所以它理应:
-
没有参数输入。
-
没有值返回。