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

1、函數

1.1 函數與方法

Scala 中函數與方法的區別很是小,若是函數做爲某個對象的成員,這樣的函數被稱爲方法,不然就是一個正常的函數。java

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

println(multi1(3)) //輸出 9
println(multi2(3)) //輸出 9

也可使用 def 定義函數:git

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

multi2multi3 本質上沒有區別,這是由於函數是一等公民,val multi2 = (x: Int) => {x * x} 這個語句至關因而使用 def 預先定義了函數,以後賦值給變量 multi2github

1.2 函數類型

上面咱們說過 multi2multi3 本質上是同樣的,那麼做爲函數它們是什麼類型的?二者的類型實際上都是 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*oop

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. 默認值參數

在定義函數時,能夠爲參數指定默認值。spa

object ScalaApp extends App {

  def detail(name: String, age: Int = 88): Unit = println(name + ":" + age)

  // 若是沒有傳遞 age 值,則使用默認值
  detail("heibaiying")
  detail("heibaiying", 12)

}

2、閉包

2.1 閉包的定義

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

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

  • 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、高階函數

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 開源項目大數據入門指南

相關文章
相關標籤/搜索