闭包与函数柯里化

闭包

闭包是一个函数,其返回值依赖于在该函数外声明的一个或多个变量的值。请看下面的代码:

var y = 3
val multiplier = (x:Int) => x * y
multiplier(2)

在上面的代码中,multiplier就是一个闭包,其返回值依赖于函数外声明的变量y。multiplier函数引用y,每次读取它的当前值。Scala编译器创建一个闭包,它包含封闭作用域中的变量。

函数的变量不在其作用域内被调用,就是闭包的概念,看下面一个例子:

def closePackage: Unit ={
    def mulBy(factor:Double) = (x:Double) => factor * x
    val triple = mulBy(3)
    val half = mulBy(0.5)
    println(triple(14) +" " + half(14))     // 42, 7
}

代码说明:

mulBy的首次调用将参数变量factor设为3,。该变量在(x:Double)=> factor *x函数的函数体内被引用。该函数被存入triple。然后参数变量factor从运行时的栈上被弹出。 mulBy再次被调用,这次factor被设为0.5.该变量在(x:Double) => factor *x函数的函数体内被引用,该函数被存入half。

函数柯里化

在本节中,我们将学习如何创建参数被组织成参数组的函数—也称为函数柯里化(Function Currying)。此外,还将介绍如何从curry过的函数中创建部分应用的函数(Partially Applied Functions)。

如果已经学习了关于函数的内容,那么现在我们应该已经熟悉使用参数定义函数了。但是,Scala也允许使用()创建函数,其中每个参数都包含在自己的组中,如下所示:

def totalCost(fruitType: String)(quantity: Int)(discount: Double): Double = {
  println(s"计算 $quantity $fruitType和 ${discount * 100}% 折扣的总成本")
  val totalCost = 2.50 * quantity
  totalCost - (totalCost * discount)
}

用参数组定义的函数通常也称为curry函数。将一个以多个参数为参数的函数转换为一个以单个参数为参数的函数的过程称为柯里化(Curring)。每个形参都包含在()中,不需要像定义带形参的函数那样用逗号分隔每个形参。例如,有一个函数func(a,b),它有两个参数a和b。对其进行柯里化转换之后,变为func(a)(b)。

当调用一个curry过的函数时,需要通过将每个形参包含在()中来填充它的形参,如下所示:

println(s"总成本 = ${totalCost("苹果")(10)(0.1)}")

完整示例代码如下:

object demo {

  // Scala也允许使用()创建函数,其中每个参数都包含在自己的组中,如下所示:
  def totalCost(fruitType: String)(quantity: Int)(discount: Double): Double = {
    println(s"计算 $quantity $fruitType 和 ${discount * 100}% 折扣的总成本")
    val totalCost = 2.50 * quantity
    totalCost - (totalCost * discount)
  }

  def main(args: Array[String]): Unit = {
    // 当调用一个curry过的函数时,需要通过将每个形参包含在()中来填充它的形参
    println(s"总成本 = ${totalCost("苹果")(10)(0.1)}")
  }
}

执行以上代码,输出结果如下:

计算 10 苹果 和 10.0% 折扣的总成本
总成本 = 22.5

因此,柯里化指的是将原来接受2个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。

// 普通函数
val add = (x: Int, y: Int) => x + y
add(3,5)

// 在Scala中,curried函数定义为多个参数列表,如下:
def add(x: Int)(y: Int) = x + y
add(4)(6)
 
// 还可以使用如下语法来定义一个柯里化函数
def add(x: Int) = (y: Int) => x + y     // 函数返回一个函数
add(7)(8)

在柯里化以后,在函数调用的过程中,就变为了两个函数连续调用的形式。在Spark源码中,也有体现,所以对()()这种形式的柯里化函数,一定要掌握。

以下函数接受一个参数,生成另一个接受单个参数的函数,要计算两个数的乘积,调用如下:

def curryingFunction ={
    def totalSum(x:Int, y:Int) = println( x + y)     

    // totalSum(4,5)

    def totalSumAtTime(x:Int) = (y:Int) => x + y   // 返回闭包
    println(totalSumAtTime(4)(5))
}

curryingFunction

输出结果如下:

9

如何从一个具有curry参数组的函数创建一个部分应用的函数?

在Scala中,函数从一开始就被设计成该语言的头等公民。为此,curried函数的一个常见应用是作为一个构建块,可以通过创建部分函数来在构建块中重用函数。

作为一个简单的例子,让我们创建一个名为totalCostForGlazedDonuts的部分应用函数,它将调用前面步骤中的curry过的totalCost()函数。

val totalCostForFruits = totalCost("苹果") _

调用部分应用的函数totalCostForFruits,与调用通过将每个参数都包含在()中来调用curry过的函数totalCost没有什么不同,但是,不需要为fruitType String参数填写第一个参数。

println(s"\n苹果的总价钱是 ${totalCostForFruits(10)(0.1)}")

完整示例如下:

object demo {

  // Scala也允许使用()创建函数,其中每个参数都包含在自己的组中,如下所示:
  def totalCost(fruitType: String)(quantity: Int)(discount: Double): Double = {
    println(s"计算 $quantity $fruitType 和 ${discount * 100}% 折扣的总成本")
    val totalCost = 2.50 * quantity
    totalCost - (totalCost * discount)
  }

  def main(args: Array[String]): Unit = {
    // 当调用一个curry过的函数时,需要通过将每个形参包含在()中来填充它的形参
    println(s"总成本 = ${totalCost("苹果")(10)(0.1)}")

    // 定义一个部分应用函数(这是一个值函数,使用val声明)
    val totalCostForFruits = totalCost("苹果") _
    
    // 调用部分应用的函数totalCostForFruits,与调用通过将每个参数都包含在()中来调用curry过的函数totalCost没有什么不同,但是,不需要为fruitType String参数填写第一个参数。
    println(s"\n苹果的总价钱是 ${totalCostForFruits(10)(0.1)}")
  }
}

执行以上代码,输出结果如下所示:

计算 10 苹果 和 10.0% 折扣的总成本
总成本 = 22.5
计算 10 苹果 和 10.0% 折扣的总成本

苹果的总价钱是 22.5

注意,部分应用的函数totalCostForFruits 的返回类型是Int => Double => Double。第一个Int是quantity形参,Double是discount形参,最后一个Double是函数的返回类型。简而言之,部分应用的函数创建了一个函数链。


《Flink原理深入与编程实战》