scala这写的都是啥?一篇文章搞清柯里化

前言

平时我们在使用scala的时候,对scala的函数真的是有爱有恨,任意地方定义,形式简单,恨的是变种太多了,不熟悉的时候,真是让人累觉不爱。针对scala的函数,先来看看他的基本定义形式:

def [类名]([参数名]:[Type]):[Type]={
}

这个样子不好看,写个demo:

def test(param1:String):String={
  
}

ok,都没有问题,很简单嘛,平时习惯了java,大概也能看懂,就跟定义java的方法有点差不多,虽然看起来和java有点不一样,无非就是参数名和返回值的顺序倒着放,再加个def 关键字嘛。于是兴冲冲的去scala项目中看源码写bug去了。

然后发现很多函数就是这一坨奇葩的代码,丫的咋和平时看的函数不一样,怎么这么多括号,比如这样

   def foldLeft[B](z: B)(f: (B, A) => B): B = {
    var acc = z
    var these = this
    while (!these.isEmpty) {
      acc = f(acc, these.head)
      these = these.tail
    }
    acc
  }

好吧,too young too naive。我还是老老实实从头看一波吧。

这奇葩写法咋来的

函数的特权

在scala定义中,函数是可以做任何事情的,那么函数就拥有了以下特性:函数可以赋值给变量;函数可以作为函数的参数;函数可以作为函数的返回值。

先来看个例子:

   def main(args: Array[String]): Unit = {

    def f() :Unit={
      println("hello function")
    }

    def f0():Unit={
      f
    }

    f0()
  }

此处,这个代码的执行逻辑是通过调用f0函数,然后最终调用到f函数,执行f函数中的方法。这和我们之前接触的java调用大同小异,只是在调用的地方因为f函数式无参函数,所以省略了括号。如果现在针对这个例子变化一下,根据函数可以作为函数的返回值这一个特点,我们如何实现将f0() 返回函数f呢?

针对这种情况,scala提供了方案,可以通过返回函数处增加特殊符号下划线实现,即:

   def main(args: Array[String]): Unit = {

    def f() :Unit={
      println("hello function")
    }
    //注意此处,返回值不能定义为Unit,因为返回的是函数
    def f0()={
      //注意此处,加下划线表示返回函数
      f _
    }

    f0()
  }

ok,根据上面的例子,我们发现f0()这儿返回的应该是f函数,那么针对f函数,它当然也可以被调用,那么这个地方继续调用

f0()()

那么执行结果"hello function"将会打印在控制台,那两个括号好像有点意思了哈,我们继续往下研究。

函数的嵌套

在scala中,任何函数都可以嵌套函数,为了简洁,以上的函数我们可以这么写:

   def main(args: Array[String]): Unit = {
    def f1()={
      def f2() :Unit={
        println("hello function")
      }
      f2 _
    }

    f1()()
  }

同理,这儿依然如期会打印出"hello function"。刚刚我们用的例子,都是无参的例子,那么,有参数的情况又会是什么样子。我们进行试验

   def main(args: Array[String]): Unit = {
    def f1(i: Int) = {
      def f2(j: Int): Int = {
        i * j
      }

      f2 _
    }

    println(f1(2)(3))
  }

结果可想而知,i=2,j=3,最终结果是 6 ,控制台打印的结果也是如此。

终于说到正题了

经过一系列既不好理解又不好看的代码,终于有个逻辑学家忍不住出手了,这位科学家叫库里 (此处我先表明友军身份:湖人总冠军!)。这位科学家一顿操作,将原来接受两个参数的函数变成新的接受一个参数的函数。这个操作也因其得名 Currying,翻译过来就是指的柯里化。私认为虽然难理解,但是好看了很多。我们还是拿上面的例子实际演示一下:

   def main(args: Array[String]): Unit = {

    def f3(i: Int)(j: Int): Int = {
      i * j
    }
    
    println(f3(2)(3))
  }

嗯,这下函数 f3(i: Int)(j: Int) 看起来像点样子了,原来是这样好几步转化而来。

小结

小结顺便补充一下scala的柯里化:柯里化(Currying)指的是把原来接受多个参数的函数变换成接受一个参数的函数过程,并且返回接受余下的参数且返回结果为一个新函数的技术。它有两种写法

    //第一种柯里化
    def f1(x: Int) = (y: Int) => x + y

    val f2 = f1(1)
    val result: Int = f2(2)
    println(result)

    //第二种柯里化
    def curryFunction(x: Int)(y: Int) = x + y
		val sum: Int = curryFunction(1)(2)
    println(sum)

什么,到这儿还没完?

当然没完,我们光是认识了柯里化,但是还有些问题没搞清呢

闭包

我们现在知道上面的f3函数式从 f1,f2函数演变而来,那么再看看原来的函数,发现一个问题

    def f1(i: Int) = {
      def f2(j: Int): Int = {
        i * j
      }
      f2 _
    }

    f1(2)(3)

我们的 f1(2)(3) 中,当f1(2)执行完毕,还未执行f2(3)的时候,里面的i的内存应该是从jvm的栈中弹出了,那么此处的i为什么还可以用呢?

​ 所以此处又将引入了一个新的概念:闭包。简单的理解就是:函数体受外部环境所影响,一段封闭的代码块将外部环境包括进来,改变了这个外部环境的生命周期。我们的柯里化,肯定会存在闭包现象,二者同气连枝。

最后,这么写有什么好处吗

当然有,德国著名哲学家曾说过:凡是存在的都是合理的。那么柯里化和闭包的存在必有其道理。我们还是以一个例子来说明:

  def main(args: Array[String]): Unit = {
    val file = makeFile(".scala")
    println(file("cat"))
    println(file("dog.scala"))
  }

  def makeFile(suffix: String)=(fileName: String) => {
    if (fileName.endsWith(suffix))
      fileName
    else
      fileName + suffix
  }

此处当我们发现 suffix可以复用的时候,采用柯里化可以使代码减少复用,更加简洁。

除此之外,柯里化还能够进行延迟计算,就像add(1)(2)一样,1比2先传入,2就会被延迟计算,在特定的场景里,有一定的应用意义。另外,柯里化对类型推演也有帮助,scala的类型推演是局部的,在同一个参数列表中后面的参数不能借助前面的参数类型进行推演,柯里化以后,放在两个参数列表里,后面一个参数列表里的参数可以借助前面一个参数列表里的参数类型进行推演。

最关键的,不得不让我们去了解的原因就是:源码中经常出现啊,看不懂怎么办啊。

本文通过scala函数的写法,特性,嵌套和简化等几方面简述了scala柯里化和闭包,希望通过本文能大概解释清楚。

posted @ 2020-03-10 16:46  西兰花是真的菜  阅读(628)  评论(1编辑  收藏  举报