1. 介绍
柯里化(currying, 以逻辑学家Haskell Brooks Curry的名字命名)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。 在Scala中方法和函数有细微的差别,通常编译器会自动完成方法到函数的转换。如果想了解Scala方法和函数的具体区别,请参考博文Scala基础 - 函数和方法的区别。
2. Scala中柯里化的形式
Scala中柯里化方法的定义形式和普通方法类似,区别在于柯里化方法拥有多组参数列表,每组参数用圆括号括起来,例如:
def multiply(x: Int)(y: Int): Int = x * y
multiply方法拥有两组参数,分别是(x: Int)和(y: Int)。
multiply方法对应的柯里化函数类型是:
Int => Int => Int
柯里化函数的类型声明是右结合的,即上面的类型等价于:
Int => (Int => Int)
表明该函数若只接受一个Int参数,则返回一个Int => Int类型的函数,这也和柯里化的过程相吻合。
3. 探究柯里化函数
我们仍以上面定义的multiply方法为例探索柯里化的一些细节:
def multiply(x: Int)(y: Int): Int = x * y
上面的代码定义了一个柯里化方法,在Scala中可以直接操纵函数,但是不能直接操纵方法,所以在使用柯里化方法前,需要将其转换成柯里化函数。最简单的方式是使用编译器提供的语法糖:
val f = multiply _
返回的函数类型是:
Int => Int => Int
使用Scala中的部分应用函数(partially applied function)技巧也可以实现转换,但是请注意转后后得到的并不是柯里化函数,而是一个接受两个(而不是两组)参数的普通函数:
val f = multiply(_: Int)(_: Int)
转后后得到的类型为:
(Int, Int) => Int
其实就是一个接受两个Int型参数的普通函数类型。
先传入第1个参数:
val f1 = f(1)
返回类型为:
Int => Int
即一个接受一个Int参数返回Int类型的函数。 继续传入第2个参数:
val f2 = f1(2)
返回类型为:
Int
两组参数都已经传入,返回一个Int类型结果。
4. 柯里化(currying)函数和部分应用函数(partial applied)的区别
下面代码定义一个普通方法multiply1
和一个currying方法multiply2
,并将其转换相应的函数类型:
def multiply1(x: Int, y:Int, z:Int) = x * y * z
val partialAppliedMultiply = multiply1 _
//类型:(x: Int, y: Int, z: Int) => Int
def multiply2(x: Int)(y: Int)(z: Int) = x * y * z
val curryingMultiply = multiply2 _
//类型:Int => (Int => (Int => Int))
在调用时,curryingMultiply可以依次传入各个参数,而partialAppliedMultiply在传入部分参数时,必须显示指定剩余参数的占位符:
val curryingMultiply1 = curryingMultiply(1)
//类型:Int => (Int => Int)
val partialAppliedMultiply1 = partialAppliedMultiply(1, _:Int, _: Int)
//类型:(Int, Int) => Int
另外,curryingMultiply1的类型仍然是currying类型,而partialAppliedMultiply1的类型仍然是普通函数类型。
5. 应用:控制抽象(Control Abstraction)
5.1 控制抽象介绍
对于一些通用的操作可以实现成控制抽象,例如像文件打开、关闭操作。实现成控制抽象的好处是,可以在使用的时候,看起来更像是语言级别提供的功能。
5.2 抽象控制的实现基础
5.2.1 无参函数
无参函数的类型是() => T
,在使用时为了简化可以省略(),例如:
def runInThread(block: => Unit){
new Thread {
override def run() { block }
}.start()
}
这样定义之后,在使用的时候就可以省略() =>,
runInThread{
println("Hi")
}
5.2.2 使用{}替代()
如果方法只有一个参数,则可以使用{}替代(),例如:
runInThread{
println("Hi")
}
5.2.3 传名参数(by-name parameter)
与传名参数相对的是传值参数。传值参数在函数调用之前表达式会被求值,例如Int,Long等数值参数类型;传名参数在函数调用前表达式不会被求值,而是会被包裹成一个匿名函数作为函数参数传递下去,例如高阶函数的函数参数就是传名参数。
5.3 控制抽象示例
withPrintWriter是一个柯里化方法,它接受两组参数,第1组参数是待操作的文件资源,第2组参数是操作文件资源的函数:
def withPrintWriter(file: File)(op: PrintWriter => Unit) {
val writer = new PrintWriter(file)
try {
op(writer)
} finally {
writer.close()
}
}
用法如下:
withPrintWriter(new File("date.txt")) {
writer => writer.println(new java.util.Date)
}
withPrintWriter确保文件资源在被使用之后一定会被关闭,并且在使用的时候,看起来更像是语言内置的关键字函数。
6. 参考
- Programming in Scala, 2nd Edition
- 快学Scala