Scala之闭包&柯里化

目录

  1. 闭包
  2. 方法与函数的区别
  3. 柯里化

闭包

说到柯里化必先说起闭包,我们先不关心闭包和柯里化是什么,而是看一个transformation

val list = List(1, 2, 3, 4, 5, 6)

val init:Int = 10

val i = list.foldLeft[Int](init)((x,y) => {
println(s"init = $init | x = $x | y = $y" )
x+y
})

println(i)
init = 10 | x = 10 | y = 1
init = 10 | x = 11 | y = 2
init = 10 | x = 13 | y = 3
init = 10 | x = 16 | y = 4
init = 10 | x = 20 | y = 5
init = 10 | x = 25 | y = 6
31

从结果来看 foldLeft 需要三个参数,init初始值,变量x,变量y,然后将他们累加(不考虑其他运算规则)

现在我们来看看闭包的解释:

  1. 闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。

  2. 闭包通常来讲可以简单的认为是可以访问一个函数里面局部变量的另外一个函数。

val i = list.foldLeft[Int](init)((x,y)) 是一个闭包,返回值依赖声明在函数外的init

如果我们把foldLeft[Int](init)((x,y)) 拆分成两个函数的话,一个是有明确参数引用的 foldLeft[Int](init), 另外一个是匿名函数((x,y)),如果我们观察仔细的话,会发现上面的代码中,第一次输出的结果 init = x 我们就可以简单的认为匿名函数((x,y))访问了flodLeft中的局部变量init ,事实上,x第一次的值就是拿的init

我们现在可以总结闭包就是在函数外面声明了一个变量,在函数中引用了这个变量,就称之为闭包,其他函数可以依赖闭包(函数)中的这个变量。由于闭包是把外部变量包了进来,所以这个变量的生命周期和闭包的生命周期一致。

最后在看一个案例:

val factor = 3
val multiplier = (i:Int) => i * factor
println(multiplier(3))

没错,函数的返回值依赖于声明在函数外部的一个或多个变量就是闭包。

方法与函数的区别

还需要讲一下方法与函数区别,因为柯里化指的是将原来接受两个参数的方法变成新的接受一个参数的函数的过程。

scala> def method = (x:Int,y:Int) => x+y
method: (Int, Int) => Int //方法
scala> val func = (x:Int,y:Int) => x+y
func: (Int, Int) => Int = $$Lambda$1307/1309775952@32a1aabf //函数
  1. 首先应该要知道=号右边的内容 (x: Int, y: Int) => x + y是一个函数体。
  2. 方法只能用def修饰,函数可以用def修饰,也可以用val修饰。
  3. 当函数用def来接收之后,不再显示为function,转换为方法。
  4. 方法可以省略参数,函数不可以。
  5. 函数可以作为方法的参数。

柯里化

理解闭包后,我们看一个复杂的闭包案例:

scala> def addBase(x:Int) = (y:Int) => x+y
addBase: (x: Int)Int => Int

首先我们看到addBase是一个方法,(y:Int) => x+y是一个函数体,下面我们试试执行addBase(x:Int)

会返回什么?

scala> addBase(3)
res27: Int => Int = $$Lambda$1308/761477414@553f7b1e

返回了一个函数,现在我们明白了,addBase(x:Int)是一个方法,在传入具体的值后,返回了一个具体的函数

到现在 “=” 号后面的 (y:Int) => x+y 这段还没有使用到,但是我们知道 “=” 号后面是一个函数体,给函数中传入y 返回 x+y? 我们试试。

scala> val addThree = addBase(3)	//使用个变量接收函数
addThree: Int => Int = $$Lambda$1308/761477414@65e1c98c
scala> addThree(4) //传入4
res29: Int = 7 //输出7

等等。。。这就是一个闭包啊,x=3是外部传入的给函数addBase的,这时的addBase就是函数,它的内部维护了一个变量x=3,这时另外一个函数addThree 在输出x+y前,访问了addBase中的x=3。

完全符合闭包的定义规则,函数的返回值依赖于声明在函数外部的一个或多个变量就是闭包

上面的addBase方法使我们一次传入一个3一次传入一个4,那么有没有办法一次传入呢?

scala> addBase(3)(4)
res34: Int = 7

没错,这就是柯里化,柯里化指的是将原来接受两个参数的方法变成新的接受一个参数的函数的过程。并且新的函数返回一个以原有第二个参数作为参数的函数。

我们发现了上面闭包的代码其实就是柯里化的过程

现在我们总结下柯里化是什么?

  1. 柯里化指的是将原来接受两个参数的方法变成新的接受一个参数的函数的过程。

    scala> def sum1(x:Int): Int => Int = (y:Int)=>x+y
    sum1: (x: Int)Int => Int

    scala> val res1 = sum1(3)
    res1: Int => Int = $$Lambda$1321/962341885@41d283de

    scala> res1(4)
    res38: Int = 7

    ==>上面是接受两个参数,下面是接受一个参数

    scala> def sum2(x:Int)(y:Int): Int = x+y
    sum2: (x: Int)(y: Int)Int

    scala> sum2(2)(3)
    res39: Int = 5
  2. 新的函数返回一个以原有第二个参数作为参数的函数。

    意思就是原来要分两步做的事情,现在分一步就做好了,底层自动调用和返回。在这个过程中,使用了闭包。

懵逼之余。。。返回最开始的foldLeft案例,看foldLeft 方法源码:

def foldLeft[B](z: B)(@deprecatedName('f) op: (B, A) => B): B

这其实就是一个柯里化应用

  1. 折叠操作是一个递归的过程,将上一次的计算结果代入到函数中

  2. 作为结果的参数在foldLeft是第一个参数,下个参数在foldRight是第二个参数

    val i = list.foldLeft[Int](init)(_+_)

    ==>init = 10 作为初始值只用一次,然后(_+_)累加,累加的结果放在第一个参数位置

    x = 10 | y = 1
    x = 11 | y = 2
    x = 13 | y = 3
    x = 16 | y = 4
    x = 20 | y = 5
    x = 25 | y = 6

总结

  1. 柯里化技术在提高适用性还是在延迟执行或者固定易变因素等方面有着重要重要的作用,加上scala语言本身就是推崇简洁编码,使得同样功能的函数在定义与转换的时候会更加灵活多样。另外在Spark的源码中有大量运用scala柯里化技术的情况,需要掌握好该技术才能看得懂相关的源代码。

  2. 在scala柯里化中,闭包也发挥着重要的作用。所谓的闭包就是变量出了函数的定义域外在其他代码块还能其作用,这样的情况称之为闭包。就上述讨论的案例而言,如果没有闭包作用,那么转换后函数其实返回的匿名函数是无法在与第一个参数x相关结合的,自然也就无法保证其所实现的功能是跟原来一致的。

Author: Tunan
Link: http://yerias.github.io/2020/03/23/scala/4/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.