1. 程式人生 > 程式設計 >Scala 系列(十)—— 函式 & 閉包 & 柯里化

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

一、函式

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 
複製程式碼

multi2multi3

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

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,3).map(_ * 3)
  // 5.使用具名函式
  val array03 = Array(1,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 開源專案大資料入門指南