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柯里化和閉包,希望通過本文能大概解釋清楚。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章