函数式编程的一个关键是函数应当是首类的(first-class)。这表示函数不仅能得到声明和调用,还可以作为一个数据类型用在这个语言的任何地方。首类函数与其他数据类型一样,可以采用字面量形式创建,而不必指定标识符;或者可以存储在一个容器中,如值、变量或数据结构;还可以用作另一个函数的参数或者另一个函数的返回值。
如果一个函数接受其他函数作为参数,或者使用函数作为返回值,就称为高阶函数。比如map()和reduce()。
1.函数类型和值
函数的类型(type)是其输入类型和返回值类型的一个简单组合,由一个箭头从输入类型指向输出类型。
语法:函数类型
([, ...]) =>
例如,函数def double(x: Int): Int = x * 2的函数类型为Int=>Int,这表示它有一个Int参数,并返回一个Int。函数名“double”是一个标识符,不是类型的一部分。
例子:创建一个函数,把它赋给一个函数值
scala> def double(x: Int): Int = x * 2
double: (x: Int)Int
scala> double(5)
res16: Int = 10
scala> val myDouble: (Int) => Int = double
myDouble: Int => Int =
scala> myDouble(10)
res17: Int = 20
scala> val myDoubleCopy = myDouble
myDoubleCopy: Int => Int =
myDouble值必须有显式类型,以区分出它是一个函数值,而不是一个函数调用。定义函数值以及用函数赋值的另一种做法是使用通配符_。
注意:有单个参数的函数类型可以省略小括号。例如,如果一个函数有一个整数参数,并返回一个整数,类型可以写为Int => Int
语法:用通配符为函数赋值
val = _
例子:
scala> def double(x: Int): Int = x * 2
double: (x: Int)Int
scala> val myDouble = double _
myDouble: Int => Int =
scala> val amount = myDouble(20)
amount: Int = 40
如果函数类型中包含多个输入,则需要再输入类型上显式地加上小括号。
例子:
scala> def max(a: Int, b: Int) = if (a > b) a else b
max: (a: Int, b: Int)Int
scala> val maximize: (Int, Int) => Int = max
maximize: (Int, Int) => Int =
scala> maximize(50, 30)
res18: Int = 50
下面给出一个没有输入的函数类型
例子:
scala> def logStart() = "=" * 50 + "\nStarting NOW\n" + "=" * 50
logStart: ()String
scala> val start: () => String = logStart
start: () => String =
scala> println( start() )
==================================================
Starting NOW
==================================================
2.高阶函数
高阶函数包含了一个函数类型的值作为输入参数或返回值。
例子:
scala> def safeStringOp(s: String, f: String => String) = {
| if (s != null) f(s) else s
| }
safeStringOp: (s: String, f: String => String)String
scala> def reverser(s: String) = s.reverse
reverser: (s: String)String
scala> safeStringOp(null, reverser)
res20: String = null
scala> safeStringOp("Ready", reverser)
res21: String = ydaeR
用函数作为参数还有另一种做法:可以使用函数字面量内联定义。
3.函数字面量
以下例子创建一个函数字面量(function literal),这是一个能正常工作的函数,但没有名字,然后把这个函数字面量赋给一个新的函数值。
scala> val doubler = (x: Int) => x * 2
doubler: Int => Int =
scala> val doubled = doubler(22)
doubled: Int = 44
函数字面量是没有名字的函数,它还有以下名字:
- 匿名函数(正式名字)
- Lambda表达式
语法:编写函数字面量
([: , ...]) =>
例子:
scala> val greeter = (name: String) => s"Hello, $name"
greeter: String => String =
scala> val hi = greeter("World")
hi: String = Hello, World
例子:将函数赋值和函数字面量做一个比较
scala> def max(a: Int, b: Int) = if (a > b) a else b
max: (a: Int, b: Int)Int
scala> val maximize: (Int, Int) => Int = max
maximize: (Int, Int) => Int =
scala> val maximize = (a: Int, b: Int) => if (a > b) a else b
maximize: (Int, Int) => Int =
scala> maximize(10, 20)
res24: Int = 20
可以在更高阶函数调用中定义函数字面量。
例子:
scala> def safeStringOp(s: String, f: String => String) = {
| if (s != null) f(s) else s
| }
safeStringOp: (s: String, f: String => String)String
scala> safeStringOp(null, (s: String) => s.reverse)
res0: String = null
scala> safeStringOp("Ready", (s: String) => s.reverse)
res1: String = ydaeR
通过占位符语法,Scala还支持更简单的表达式。
4.占位符语法
占位符语法是函数字面量的一种缩写形式,将命名参数替换为通配符(_)。可以在以下情况使用:
(1)函数的显式类型在字面量之外指定;
(2)参数最多只使用一次。
例子:将一个函数字面量加倍,这里使用通配符取代命名参数
scala> val doubler: Int => Int = _ * 2
doubler: Int => Int =
例子:用占位符语法进行调用
scala> def safeStringOp(s: String, f: String => String) = {
| if (s != null) f(s) else s
| }
safeStringOp: (s: String, f: String => String)String
scala> safeStringOp(null,_.reverse)
res2: String = null
scala> safeStringOp("Ready",_.reverse)
res3: String = ydaeR
例子:占位符的顺序有什么影响,这个例子使用了两个占位符
scala> def combination(x: Int,y: Int,f: (Int,Int) => Int) = f(x, y)
combination: (x: Int, y: Int, f: (Int, Int) => Int)Int
scala> combination(23,12,_ * _)
res4: Int = 276
//占位符的个数必须与输入参数个数一致
scala> def tripleOp(a: Int, b: Int, c: Int, f: (Int,Int,Int) => Int) = f(a,b,c)
tripleOp: (a: Int, b: Int, c: Int, f: (Int, Int, Int) => Int)Int
scala> tripleOp(23, 92, 14, _ * _ + _)
res5: Int = 2130
下面使用两个类型参数重新定义tripleOp函数,一个表示通用输入类型,另一个表示返回值类型。这样一来,我们可以使用任何类型的输入或者我们选择的匿名函数(只要这个匿名函数有3个输入)来调用tripleOp函数:
例子:
scala> def tripleOp[A,B](a:A,b:A,c:A,f: (A,A,A)=>B) = f(a,b,c)
tripleOp: [A, B](a: A, b: A, c: A, f: (A, A, A) => B)B
scala> tripleOp[Int,Int](23,92,14,_ * _ + _)
res6: Int = 2130
scala> tripleOp[Int,Double](23, 92, 14, 1.0 * _ / _ / _)
res7: Double = 0.017857142857142856
scala> tripleOp[Int,Boolean](93, 92, 14, _ > _ + _)
res8: Boolean = false
5.部分应用函数和柯里化
调用函数时(包括常规函数和高阶函数),通常要在调用中指定函数的所有参数(包含默认参数值的函数例外)。如果你想重用一个函数调用,而且希望保留一些参数不想再次输入,怎么办?
例子:这个函数检查给定的数是否是另一个数的因数
scala> def factorOf(x: Int, y: Int) = y % x == 0
factorOf: (x: Int, y: Int)Boolean
如果需要这个函数的一个快捷方式,所有参数都不打算保留,可以使用通配符(_)赋值:
scala> val f = factorOf _
f: (Int, Int) => Boolean =
scala> val x = f(7, 20)
x: Boolean = false
如果想保留一些参数,可以部分应用这个函数,使用通配符替代其中一个参数。这里通配符需要一个显式类型,因为它要用于生成一个函数值:
scala> val multipleOf3 = factorOf(3, _:Int)
multipleOf3: Int => Boolean =
scala> val y = multipleOf3(78)
y: Boolean = true
要部分应用函数,还有一种更简洁的方法:可以使用多个参数表的函数。不是将一个参数表分解为应用参数和非应用参数,而是应用一个参数表中的参数,另一个参数不应用。这种技术称为函数柯里化:
scala> def factorOf(x: Int)(y: Int) = y % x == 0
factorOf: (x: Int)(y: Int)Boolean
scala> val isEven = factorOf(2) _
isEven: Int => Boolean =
scala> val z = isEven(32)
z: Boolean = true
从函数类型来讲,有多个参数表的函数可以认为是多个函数的一个链。单个参数表则认为是一个单独的函数调用。
示例函数def factorOf(x: Int, y:Int)的函数类型为(Int,Int) => Boolean,更新的示例函数def factorOf(x: Int)(y: Int)的函数类型为Int => Int => Boolean。通过柯里化,函数类型变成第二个串链函数Int => Boolean。函数值isEven将串链函数的第一部分柯里化为整数值2.
6.传名参数
函数类型参数还有一种形式:传名(by-name)参数。传名参数可以取一个值,也可以取最终返回一个值的函数。由于同时支持使用值来调用以及使用函数来调用,所以如果函数有一个传名参数,将由调用者来决定究竟选择使用值还是使用函数调用。
语法:指定传名参数
: =>
例子:试着调用有一个传名参数的函数。先用常规的值调用这个函数,然后再用一个函数来调用,从而验证每次访问参数时都会调用这个函数。
scala> def doubles(x: => Int) = { //1.这里可以访问x传名参数,就像访问常规的传值参数一样
| println("Now doubling" + x)
| x * 2
| }
doubles: (x: => Int)Int
scala> doubles(5) //2.用一个常规值调用doubles方法,将正常操作
Now doubling5
res0: Int = 10
scala> def f(i: Int) = { println(s"Hello from f($i)");i }
f: (i: Int)Int
scala> doubles( f(8) ) //3.用一个函数值调用这个方法时,会在doubles方法中调用这个函数值。
Hello from f(8)
Now doubling8
Hello from f(8) //4.由于double方法引用了x参数两次,所以“Hello”消息会调用两次
res1: Int = 16
7.偏函数
目前为止我们研究的所有函数都称为全函数(total functions),因为它们能正确地支持满足输入参数类型的所有可能的值。
不过,有些函数并不能支持满足输入类型的所有可能的值。例如,如果一个函数返回输入数的平方根,如果这个输入数为负数,它就不能工作。这种函数称为偏函数(partial functions),因为它们只能部分应用于输入数据。
Scala的偏函数时可以对输入应用一系列case模式的函数字面量,要求输入至少与给定的模式之一匹配。调用一个偏函数时,如果所使用的数据不能满足其中至少一个case模式,就会导致一个Scala错误。
例子:偏函数
scala> val statusHandler: Int => String = {
| case 200 => "Okay"
| case 400 => "Your Error"
| case 500 => "Our error"
| }
statusHandler: Int => String =
//这个偏函数只能应用于值为200、400和500的整数。
scala> statusHandler(200)
res2: String = Okay
scala> statusHandler(400)
res3: String = Your Error
scala> statusHandler(401)
scala.MatchError: 401 (of class java.lang.Integer)
at $anonfun$1.apply(:11)
at $anonfun$1.apply(:11)
... 32 elided
偏函数在处理集合和模式匹配时更为有用。
8.用函数字面量块调用高阶函数
高阶函数除了用小括号,还可以使用函数字面量块来调用高阶函数,或者用函数字面量块取代小括号。用函数名和一个大的表达式块调用的函数将这个块作为一个函数字面量,它会调用0次或多次。这种语法的一种常见用法是用一个表达式块调用工具函数。例如,一个高阶函数可以把给定的表达式块放在一个数据库会话或事务中。
例子:常规函数字面量
def safeStringOp(s: String, f: String => String) = {
if (s != null) f(s) else s
}
val uuid = java.util.UUID.randomUUID.toString //1.访问UUID工具
val timedUUID = safeStringOp(uuid, { s =>
val now = System.currentTimeMillis
val timed = s.take(24) + now
timed.toUpperCase
})
可以对以上的例子做些改进,把参数分为两个单独的组,第二个参数组(包含函数类型)可以用表达式块语法来调用:
def safeStringOp(s: String)(f: String => String) = {
if (s != null) f(s) else s
}
val timedUUID = safeStringOp(uuid){ s =>
val now = System.currentTimeMillis
val timed = s.take(4) + now
timed.toUpperCase
}
现在有了一个更清晰的函数调用,值参数放在小括号里传入,函数参数作为一个独立的函数字面量块传入。
例子:传名参数,使用一个类型参数作为这个传名参数返回类型以及主函数的返回类型
def timer[A](f: => A): A = { //1.类型参数A使得f传名参数的返回类型称为timer函数的返回类型,从而减少用timer函数包围代码的影响
def now = System.currentTimeMillis
val start = now; val a = f; val end = now
println(s"Executed in ${end - start} ms")
a
}
val veryRandomAmount = timer { //2.将高阶函数的表达式块语法规约为最简单的形式:函数名和块。
util.Random.setSeed(System.currentTimeMillis)
for (i <- 1 to 100000) util.Random.nextDouble
util.Random.nextDouble
}
这里使用timer函数包围了一个单独的代码单元,不过也可以集成到一个现有的代码基中。可以用它来包围任何函数的最后一部分,测量其性能,同时确保从代码块传递的函数返回值经过“timer”并由函数返回。
函数可以采用这种方式将单独的代码块包围在工具函数中,这也是使用“表达式块”型高阶函数调用的主要好处。使用这种调用还有另外一些好处,包括:
- 管理数据库事务,即高阶函数打开会话、调用函数参数, 然后用一个commit或rollback结束事务。
- 重新尝试处理可能的错误,将函数参数调用指定次数,直到不再产生错误。
- 根据局部、全局或外部值(例如,一个数据库设置或环境变量)有条件地调用函数参数。