CH02-函数
高阶函数-把函数传递给函数
函数也是值,也可以赋值给一个变量、存储在一个数据结构里、像参数一样传递给另一个函数。
循环调用
使用循环方式实现阶乘:
def factirial(n:Int): Int = {
def go(n:Int, acc:Int):Int = { // 内部函数,跟一个函数内的变量含义相同
if (n <= 0) acc
else go(n-1, n * acc)
}
go(n, 1)
}
想不通过修改一个循环变量而实现循环功能,可以借助递归函数。这样的递归函数一般没命名为 go 或 loop。
上面的例子中,内部函数go实现循环,接收下一个n和和累积值acc。要退出循环时,返回一个不继续递归的值,即n <= 0。
编译器会检测到这种自递归,只要递归调用发生在尾部,即递归调用后面没有其他的调用,编译器会优化成类似while循环的字节码。
尾调用
指调用者在一个递归调用之后不做其他事,只是返回这个调用结果。
比如
else go(n-1, n * acc)部分,是直接返回了go的递归调用,没有再做其他计算。如果是1 + go(n-1, n * acc),则不再是尾调用,因为又进行了计算。如果递归调用在尾部位置,则会自动编译为循环迭代,即不会每次进行栈的操作。可以通过
@annotation.tailrec告诉编译器当没有成功编译成循环迭代时发出警告。
高阶函数
def formatResult(name:String, n:Int, f: Int => Int) = {
val msg = "The %s of %d is %d."
msg.format(name, n, f(n))
}
这里的参数f,其类型为Int => Int,表示接受一个整数并返回一个整数的函数。
因为高阶函数一般不能通过函数名来准确表示函数的功能,因此使用较短的函数命名,比如 g、h、f 等。
多态函数-基于类型的抽象
这里的多态跟继承中所说的“父类-子类继承”不同,这里是指类型的多态。
之前见到的函数都是单态的,因为函数只操作一种数据类型。适用于多种数据类型的函数,称为多态函数,或泛型函数。
多态函数的构建,一般是发现多个单态函数有相似的结构,这时,可以封装为一个多态函数。
实例
比如一个函数,返回数组中第一个匹配到 key 的索引,否则返回 -1:
def findFirst(ss:Array[String], key:String):Int = {
@annotatin.tailrec
def loop(n:Int):Int = {
if (n >= ss.length) -1 // 到达数组尾部仍未匹配到 key,返回 -1
else if (ss(n) == key) n // 匹配到 key,返回索引
else loop(n + 1) // 递归调用
}
}
这是从 String 数组中匹配,如果是从 Int 数组查找匹配,也是类似的结构,因此我们就可以将它改写为一个从 A 类型数组中查找对应索引的函数:
def findFirst[A](as:Array[A], p:A => Boolean):Int = {
@annotatin.tailrec
def loop(n:Int):Int = {
if (n >= as.length) -1
else if p(as(n)) n // 使用传入的 测试函数 p 对当前元素进行判断
else loop(n + 1)
}
}
函数名后跟的是类型参数,由中括号包围,多个参数使用逗号分隔,一般使用单个大写字母表示一个类型参数。这些类型参数作为类型变量,可以在其他类型签名中引用,比如上面的as参数类型。
向高阶函数传入匿名函数
在调用高阶函数时,并没有必要提前定义一个有名函数然后再传入,可以在调用时直接定义一个函数值作为高阶函数的参数,这被称为匿名函数或函数字面量。比如:
findFirst(Array(1,2,3), (x:Int) => x == 9)
(x:Int) => x == 9即为一个匿名函数,或称为函数字面量。
匿名函数中,=>左边为该函数的参数列表,右边则会函数体。如果 Scala 可以推断参数的类型,则可以省略掉。
函数也是值
当定义一个函数字面量时,实际上是定义了一个包含一个
apply方法的 Scala 对象。而当一个对象拥有apply方法,则可以把该对象当做方法一样调用。比如我们定义一个函数字面量
(a, b) => a < b,它事实上是创建函数对象的语法糖:val lessThan = new Function2[Int, Int, Boolean] { def apply(a:Int, b:Int) = a < b }
lessThan的类型为Function2[Int, Int, Boolean],通常会写成(a, b) => Boolean。Function2特质拥有一个apply方法,在调用lessThan(10, 20)时实际上是对apply方法调用的语法糖:lessThan.apply(10, 20) // true这些类似
Function2的特质,实际是由 Scala 标准库提供的普通特质,比如Function1、Funciton3等等。其中的数字是指接收的参数个数。因为这些函数在 Scala 中是普通对象,因此他们也是一等值。
通过类型实现多态
在实现多态函数时,各种可能的实现方式明显比普通函数减少了。比如针对类型 A 的多态函数,唯一能够对 A 进行操作的方式是传入一个函数作为参数,通过这个传入的函数来操作 A。
比如一个例子,这个函数签名表示它只有一种实现方式。它是一个执行部分应用的高阶函数。函数partial1接收一个值和一个带有两个参数的函数,并返回一个带有一个参数的函数。
部分应用函数,表示函数被应用的参数并不是它需要的完整参数,即只提供了参数列表中的部分参数。
def partial1[A, B, C](a:A, f: (A, B) => c): B => C
我们该如何实现这个高阶函数呢。根据它的返回值类型,是接收一个类型 B 的参数并返回一个类型 C 的值的函数:
def partial1[A, B, C](a:A, f: (A, B) => c): B => C = {
(b: B) => ??? // 根据返回值类型 B => C ,定义一个该返回值类型的函数
}
现在如何来实现方法体部分呢,根据声明,这个函数必须返回一个 C 类型的值。而在partial1的参数列表中,正好有一个函数f能够返回一个 C 类型的值。除此之外,我们没有其他方式来实现该函数体。因此:
def partial1[A, B, C](a:A, f: (A, B) => c): B => C = {
(b: B) => f(a, b) // 调用参数中的函数,实现符合返回类型的函数体
}
这也就是部分应用函数的实现过程,一个函数,接收两个参数,返回一个值。当我们只提供一个参数值时,在这个例子中,是a,这时会得到一个 “接收一个参数并返回一个值的” 函数。然后在提供原始函数需要的第二个参数,即b,就能得到最终的结果, 即c。
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.