Scala 導覽

高階函式

語言

高階函式將其他函式作為參數,或傳回函式作為結果。這是可能的,因為函式在 Scala 中是一等值。術語在這個時候可能會有點混淆,我們使用短語「高階函式」來表示將函式作為參數或傳回函式的函式和方法。

在純粹的物件導向世界中,一個良好的做法是避免公開參數化函式的函式,這些函式可能會洩漏物件的內部狀態。洩漏內部狀態可能會破壞物件本身的不變式,從而違反封裝。

最常見的範例之一是高階函式 map,它可用於 Scala 中的集合。

val salaries = Seq(20_000, 70_000, 40_000)
val doubleSalary = (x: Int) => x * 2
val newSalaries = salaries.map(doubleSalary) // List(40000, 140000, 80000)

doubleSalary 是一個函數,它接受一個 Int,x,並傳回 x * 2。一般來說,箭頭 => 左邊的元組是一個參數清單,而右邊表達式的值就是傳回的內容。在第 3 行,函數 doubleSalary 套用在薪資清單中的每個元素。

為了縮小程式碼,我們可以讓函數匿名並直接傳遞給 map 作為參數

val salaries = Seq(20_000, 70_000, 40_000)
val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000)

請注意,在上述範例中,x 沒有宣告為 Int。這是因為編譯器可以根據 map 預期的函數類型推斷出類型(請參閱 柯里化)。撰寫相同程式碼段更慣用的方式如下

val salaries = Seq(20_000, 70_000, 40_000)
val newSalaries = salaries.map(_ * 2)

由於 Scala 編譯器已經知道參數的類型(一個 Int),你只需要提供函數的右側。唯一的注意事項是,你需要使用 _ 取代參數名稱(在先前的範例中是 x)。

將方法強制轉換為函數

也可以將方法傳遞給高階函數作為參數,因為 Scala 編譯器會將方法強制轉換為函數。

case class WeeklyWeatherForecast(temperatures: Seq[Double]) {

  private def convertCtoF(temp: Double) = temp * 1.8 + 32

  def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF
}
case class WeeklyWeatherForecast(temperatures: Seq[Double]):

  private def convertCtoF(temp: Double) = temp * 1.8 + 32

  def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF

在此,方法 convertCtoF 傳遞給高階函數 map。這是可能的,因為編譯器會將 convertCtoF 強制轉換為函數 x => convertCtoF(x)(注意:x 會是一個產生的名稱,保證在其範圍內是唯一的)。

接受函數的函數

使用高階函數的一個原因是減少重複的程式碼。假設你想要一些方法,可以將某人的薪資提高不同的倍數。如果不建立高階函數,它可能看起來像這樣

object SalaryRaiser {

  def smallPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * salary)
}
object SalaryRaiser:

  def smallPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * salary)

請注意,這三個方法只在乘法因子方面有所不同。為了簡化,你可以將重複的程式碼萃取到一個高階函數中,如下所示

object SalaryRaiser {

  private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] =
    salaries.map(promotionFunction)

  def smallPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * salary)
}
object SalaryRaiser:

  private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] =
    salaries.map(promotionFunction)

  def smallPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * salary)

新方法 promotion,採用薪資加上一個型別為 Double => Double 的函式(也就是一個接收 Double 並回傳 Double 的函式),並回傳乘積。

方法和函式通常會表達行為或資料轉換,因此擁有可根據其他函式組合的函式,有助於建構泛用機制。這些泛用操作會延後鎖定整個操作行為,讓客戶端能夠控制或進一步自訂操作本身的部份。

回傳函式的函式

在某些情況下,您會想要產生一個函式。以下是回傳函式的其中一個方法範例。

def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
  val schema = if (ssl) "https://" else "http://"
  (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
}

val domainName = "www.example.com"
def getURL = urlBuilder(ssl=true, domainName)
val endpoint = "users"
val query = "id=1"
val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String
def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String =
  val schema = if ssl then "https://" else "http://"
  (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"

val domainName = "www.example.com"
def getURL = urlBuilder(ssl=true, domainName)
val endpoint = "users"
val query = "id=1"
val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String

請注意 urlBuilder 的回傳型別 (String, String) => String。這表示回傳的匿名函式會接收兩個字串,並回傳一個字串。在此情況下,回傳的匿名函式為 (endpoint: String, query: String) => s"https://www.example.com/$endpoint?$query"

此頁面的貢獻者