Scala 系列(十)—— 函數 & 閉包 & 柯里化

  • 2019 年 10 月 3 日
  • 筆記

一、函數

1.1 函數與方法

Scala 中函數與方法的區別非常小,如果函數作為某個對象的成員,這樣的函數被稱為方法,否則就是一個正常的函數。

// 定義方法  def multi1(x:Int) = {x * x}  // 定義函數  val multi2 = (x: Int) => {x * x}    println(multi1(3)) //輸出 9  println(multi2(3)) //輸出 9

也可以使用 def 定義函數:

def multi3 = (x: Int) => {x * x}  println(multi3(3))  //輸出 9 

multi2 和 multi3 本質上沒有區別,這是因為函數是一等公民,val multi2 = (x: Int) => {x * x} 這個語句相當於是使用 def 預先定義了函數,之後賦值給變數 multi2。

1.2 函數類型

上面我們說過 multi2 和 multi3 本質上是一樣的,那麼作為函數它們是什麼類型的?兩者的類型實際上都是 Int => Int,前面一個 Int 代表輸入參數類型,後面一個 Int 代表返回值類型。

scala> val multi2 = (x: Int) => {x * x}  multi2: Int => Int = $$Lambda$1092/594363215@1dd1a777    scala> def multi3 = (x: Int) => {x * x}  multi3: Int => Int    // 如果有多個參數,則類型為:(參數類型,參數類型 ...)=>返回值類型  scala> val multi4 = (x: Int,name: String) => {name + x * x }  multi4: (Int, String) => String = $$Lambda$1093/1039732747@2eb4fe7

1.3 一等公民&匿名函數

在 Scala 中函數是一等公民,這意味著不僅可以定義函數並調用它們,還可以將它們作為值進行傳遞:

import scala.math.ceil  object ScalaApp extends App {    // 將函數 ceil 賦值給變數 fun,使用下劃線 (_) 指明是 ceil 函數但不傳遞參數    val fun = ceil _    println(fun(2.3456))  //輸出 3.0    }

在 Scala 中你不必給每一個函數都命名,如 (x: Int) => 3 * x 就是一個匿名函數:

object ScalaApp extends App {    // 1.匿名函數    (x: Int) => 3 * x    // 2.具名函數    val fun = (x: Int) => 3 * x    // 3.直接使用匿名函數    val array01 = Array(1, 2, 3).map((x: Int) => 3 * x)    // 4.使用佔位符簡寫匿名函數    val array02 = Array(1, 2, 3).map(_ * 3)    // 5.使用具名函數    val array03 = Array(1, 2, 3).map(fun)    }

1.4 特殊的函數表達式

1. 可變長度參數列表

在 Java 中如果你想要傳遞可變長度的參數,需要使用 String ...args 這種形式,Scala 中等效的表達為 args: String*。

object ScalaApp extends App {    def echo(args: String*): Unit = {      for (arg <- args) println(arg)    }    echo("spark","hadoop","flink")  }  // 輸出  spark  hadoop  flink

2. 傳遞具名參數

向函數傳遞參數時候可以指定具體的參數名。

object ScalaApp extends App {      def detail(name: String, age: Int): Unit = println(name + ":" + age)      // 1.按照參數定義的順序傳入    detail("heibaiying", 12)    // 2.傳遞參數的時候指定具體的名稱,則不必遵循定義的順序    detail(age = 12, name = "heibaiying")    }

3. 默認值參數

在定義函數時,可以為參數指定默認值。

object ScalaApp extends App {      def detail(name: String, age: Int = 88): Unit = println(name + ":" + age)      // 如果沒有傳遞 age 值,則使用默認值    detail("heibaiying")    detail("heibaiying", 12)    }

二、閉包

2.1 閉包的定義

var more = 10  // addMore 一個閉包函數:因為其捕獲了自由變數 more 從而閉合了該函數字面量  val addMore = (x: Int) => x + more

如上函數 addMore 中有兩個變數 x 和 more:

  • x : 是一個綁定變數 (bound variable),因為其是該函數的入參,在函數的上下文中有明確的定義;
  • more : 是一個自由變數 (free variable),因為函數字面量本生並沒有給 more 賦予任何含義。

按照定義:在創建函數時,如果需要捕獲自由變數,那麼包含指向被捕獲變數的引用的函數就被稱為閉包函數。

2.2 修改自由變數

這裡需要注意的是,閉包捕獲的是變數本身,即是對變數本身的引用,這意味著:

  • 閉包外部對自由變數的修改,在閉包內部是可見的;
  • 閉包內部對自由變數的修改,在閉包外部也是可見的。
// 聲明 more 變數  scala> var more = 10  more: Int = 10    // more 變數必須已經被聲明,否則下面的語句會報錯  scala> val addMore = (x: Int) => {x + more}  addMore: Int => Int = $$Lambda$1076/1844473121@876c4f0    scala> addMore(10)  res7: Int = 20    // 注意這裡是給 more 變數賦值,而不是重新聲明 more 變數  scala> more=1000  more: Int = 1000    scala> addMore(10)  res8: Int = 1010

2.3 自由變數多副本

自由變數可能隨著程式的改變而改變,從而產生多個副本,但是閉包永遠指向創建時候有效的那個變數副本。

// 第一次聲明 more 變數  scala> var more = 10  more: Int = 10    // 創建閉包函數  scala> val addMore10 = (x: Int) => {x + more}  addMore10: Int => Int = $$Lambda$1077/1144251618@1bdaa13c    // 調用閉包函數  scala> addMore10(9)  res9: Int = 19    // 重新聲明 more 變數  scala> var more = 100  more: Int = 100    // 創建新的閉包函數  scala> val addMore100 = (x: Int) => {x + more}  addMore100: Int => Int = $$Lambda$1078/626955849@4d0be2ac    // 引用的是重新聲明 more 變數  scala> addMore100(9)  res10: Int = 109    // 引用的還是第一次聲明的 more 變數  scala> addMore10(9)  res11: Int = 19    // 對於全局而言 more 還是 100  scala> more  res12: Int = 100

從上面的示例可以看出重新聲明 more 後,全局的 more 的值是 100,但是對於閉包函數 addMore10 還是引用的是值為 10 的 more,這是由虛擬機來實現的,虛擬機會保證 more 變數在重新聲明後,原來的被捕獲的變數副本繼續在堆上保持存活。

三、高階函數

3.1 使用函數作為參數

定義函數時候支援傳入函數作為參數,此時新定義的函數被稱為高階函數。

object ScalaApp extends App {      // 1.定義函數    def square = (x: Int) => {      x * x    }      // 2.定義高階函數: 第一個參數是類型為 Int => Int 的函數    def multi(fun: Int => Int, x: Int) = {      fun(x) * 100    }      // 3.傳入具名函數    println(multi(square, 5)) // 輸出 2500      // 4.傳入匿名函數    println(multi(_ * 100, 5)) // 輸出 50000    }

3.2 函數柯里化

我們上面定義的函數都只支援一個參數列表,而柯里化函數則支援多個參數列表。柯里化指的是將原來接受兩個參數的函數變成接受一個參數的函數的過程。新的函數以原有第二個參數作為參數。

object ScalaApp extends App {    // 定義柯里化函數    def curriedSum(x: Int)(y: Int) = x + y    println(curriedSum(2)(3)) //輸出 5  }

這裡當你調用 curriedSum 時候,實際上是連著做了兩次傳統的函數調用,實際執行的柯里化過程如下:

  • 第一次調用接收一個名為 x 的 Int 型參數,返回一個用於第二次調用的函數,假設 x 為 2,則返回函數 2+y;
  • 返回的函數接收參數 y,並計算並返回值 2+3 的值。

想要獲得柯里化的中間返回的函數其實也比較簡單:

object ScalaApp extends App {    // 定義柯里化函數    def curriedSum(x: Int)(y: Int) = x + y    println(curriedSum(2)(3)) //輸出 5      // 獲取傳入值為 10 返回的中間函數 10 + y    val plus: Int => Int = curriedSum(10)_    println(plus(3)) //輸出值 13  }

柯里化支援多個參數列表,多個參數按照從左到右的順序依次執行柯里化操作:

object ScalaApp extends App {    // 定義柯里化函數    def curriedSum(x: Int)(y: Int)(z: String) = x + y + z    println(curriedSum(2)(3)("name")) // 輸出 5name    }

參考資料

  1. Martin Odersky . Scala 編程 (第 3 版)[M] . 電子工業出版社 . 2018-1-1
  2. 凱.S.霍斯特曼 . 快學 Scala(第 2 版)[M] . 電子工業出版社 . 2017-7

更多大數據系列文章可以參見 GitHub 開源項目: 大數據入門指南

Tags: