鍍金池/ 教程/ Scala/ 模式匹配與匿名函數(shù)
高階函數(shù)與 DRY
序列提取
譯者結(jié)語(yǔ)
類型 Future
類型 Option
Scala 初學(xué)指南
類型類
模式匹配與匿名函數(shù)
路徑依賴類型
提取器
類型 Either
Try 與錯(cuò)誤處理
介紹
實(shí)戰(zhàn)中的 Promise 和 Future
柯里化和部分函數(shù)應(yīng)用
無(wú)處不在的模式

模式匹配與匿名函數(shù)

上一章總結(jié)了模式在 Scala 中的幾種用法,最后提到了匿名函數(shù)。 這一章,我們具體的去學(xué)習(xí)如何在匿名函數(shù)中使用模式。

如果你參與過(guò) Coursera 上的 那門 Scala 課程 , 或者寫過(guò) Scala 代碼,那很可能你已經(jīng)熟悉匿名函數(shù)。 比如說(shuō),將一組歌名轉(zhuǎn)換成小寫格式,你可能會(huì)定義一個(gè)匿名函數(shù)傳遞給 map 方法:

val songTitles = List("The White Hare", "Childe the Hunter", "Take no Rogues")
songTitles.map(t => t.toLowerCase)

或者,利用 Scala 的 占位符語(yǔ)法(placeholder syntax) 得到更加簡(jiǎn)短的代碼:

songTitles.map(_.toLowerCase)

目前為止,一切都很順利。 不過(guò),讓我們來(lái)看一個(gè)稍微有些區(qū)別的例子: 假設(shè)有一個(gè)由二元組組成的序列,每個(gè)元組包含一個(gè)單詞,以及對(duì)應(yīng)的詞頻, 我們的目標(biāo)就是去除詞頻太高或者太低的單詞,只保留中間地帶的。 需要寫出這樣一個(gè)函數(shù):

wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String]

一個(gè)很直觀的解決方案是使用 filtermap 函數(shù):

val wordFrequencies = ("habitual", 6) :: ("and", 56) :: ("consuetudinary", 2) ::
  ("additionally", 27) :: ("homely", 5) :: ("society", 13) :: Nil
def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
  wordFrequencies.filter(wf => wf._2 > 3 && wf._2 < 25).map(_._1)
wordsWithoutOutliers(wordFrequencies) // List("habitual", "homely", "society")

這個(gè)解法有幾個(gè)問(wèn)題。 首先,訪問(wèn)元組字段的代碼不好看,如果我們可以直接解構(gòu)出字段,那代碼可能更加美觀和可讀。

幸好,Scala 提供了另外一種寫匿名函數(shù)的方式:模式匹配形式的匿名函數(shù), 它是由一系列模式匹配樣例組成的,正如模式匹配表達(dá)式那樣,不過(guò)沒(méi)有 match 。 下面是重寫后的代碼:

def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
 wordFrequencies.filter { case (_, f) => f > 3 && f < 25 } map { case (w, _) => w }

在兩個(gè)匿名函數(shù)里,我們只使用了一個(gè)匹配案例,因?yàn)槲覀冎肋@個(gè)樣例總是會(huì)匹配成功, 要解構(gòu)的數(shù)據(jù)類型在編譯期就確定了,沒(méi)有會(huì)出錯(cuò)的可能。 這是模式匹配型匿名函數(shù)的一個(gè)非常常見(jiàn)的用法。

如果把這些匿名函數(shù)賦給其他值,你也會(huì)看到它們有著正確的類型:

val predicate: (String, Int) => Boolean = { case (_, f) => f > 3 && f < 25 }
val transformFn: (String, Int) => String = { case (w, _) => w }

不過(guò)要注意,必須顯示的聲明值的類型,因?yàn)?Scala 編譯器無(wú)法從匿名函數(shù)中推導(dǎo)出其類型。

當(dāng)然,也可以定義一系列更加復(fù)雜的的匹配案例。 但是你必須的確保對(duì)于每一個(gè)可能的輸入,都會(huì)有一個(gè)樣例能夠匹配成功, 不然,運(yùn)行時(shí)會(huì)拋出 MatchError 。

偏函數(shù)

有時(shí)候可能會(huì)定義一個(gè)只處理特定輸入的函數(shù)。 這樣的一種函數(shù)能幫我們解決 wordsWithoutOutliers 中的另外一個(gè)問(wèn)題: 在 wordsWithoutOutliers 中,我們首先過(guò)濾給定的序列,然后對(duì)剩下的元素進(jìn)行映射, 這種處理方式需要遍歷序列兩次。 如果存在一種解法只需要遍歷一次,那不僅可以節(jié)省一些 CPU,還會(huì)使得代碼更簡(jiǎn)潔,更具有可讀性。

Scala 集合的 API 有一個(gè)叫做 collect 的方法,對(duì)于 Seq[A] ,它有如下方法簽名:

def collect[B](pf: PartialFunction[A, B]): Seq[B]

這個(gè)方法將給定的 偏函數(shù)(partial function) 應(yīng)用到序列的每一個(gè)元素上, 最后返回一個(gè)新的序列 - 偏函數(shù)做了 filtermap 要做的事情。

那偏函數(shù)到底是什么呢? 概括來(lái)說(shuō),偏函數(shù)是一個(gè)一元函數(shù),它只在部分輸入上有定義, 并且允許使用者去檢查其在一個(gè)給定的輸入上是否有定義。 為此,特質(zhì) PartialFunction 提供了一個(gè) isDefinedAt 方法。 事實(shí)上,類型 PartialFunction[-A, +B] 擴(kuò)展了類型 (A) => B (一元函數(shù),也可以寫成 Function1[A, B] )。 模式匹配型的匿名函數(shù)的類型就是 PartialFunction 。

依據(jù)繼承關(guān)系,將一個(gè)模式匹配型的匿名函數(shù)傳遞給接受一元函數(shù)的方法(如:map、filter)是沒(méi)有問(wèn)題的, 只要這個(gè)匿名函數(shù)對(duì)于所有可能的輸入都有定義。

不過(guò) collect 方法接受的函數(shù)只能是 PartialFunction[A, B] 類型的。 對(duì)于序列中的每一個(gè)元素,首先檢查偏函數(shù)在其上面是否有定義, 如果沒(méi)有定義,那這個(gè)元素就直接被忽略掉, 否則,就將偏函數(shù)應(yīng)用到這個(gè)元素上,返回的結(jié)果加入結(jié)果集。

現(xiàn)在,我們來(lái)重構(gòu) wordsWithoutOutliers ,首先定義需要的偏函數(shù):

val pf: PartialFunction[(String, Int), String] = {
  case (word, freq) if freq > 3 && freq < 25 => word
}

我們?yōu)檫@個(gè)案例加入了 守衛(wèi)語(yǔ)句,不在區(qū)間里的元素就沒(méi)有定義。

除了使用上面的這種方式,還可以顯示的擴(kuò)展 PartialFunction 特質(zhì):

val pf = new PartialFunction[(String, Int), String] {
  def apply(wordFrequency: (String, Int)) = wordFrequency match {
    case (word, freq) if freq > 3 && freq < 25 => word
  }
  def isDefinedAt(wordFrequency: (String, Int)) = wordFrequency match {
    case (word, freq) if freq > 3 && freq < 25 => true
    case _ => false
  }
}

當(dāng)然,前一種方法更為更為簡(jiǎn)潔。

把定義好的 pf 傳遞給 map 函數(shù),能夠通過(guò)編譯期,但運(yùn)行時(shí)會(huì)拋出 MatchError , 因?yàn)槲覀兊钠瘮?shù)并不是在所有輸入值上都有定義:

wordFrequencies.map(pf) // will throw a MatchError

不過(guò),把它傳遞給 collect 函數(shù)就能得到想要的結(jié)果:

wordFrequencies.collect(pf) // List("habitual", "homely", "society")

這個(gè)結(jié)果和我們最初的實(shí)現(xiàn)所得到的結(jié)果是一樣的,因此我們可以重寫 wordsWithoutOutliers

def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
  wordFrequencies.collect { case (word, freq) if freq > 3 && freq < 25 => word }

偏函數(shù)還有其他一些有用的性質(zhì),比如說(shuō),它們可以被直接串聯(lián)起來(lái),實(shí)現(xiàn)函數(shù)式的 責(zé)任鏈模式 (源自于面向?qū)ο蟪淌皆O(shè)計(jì))。

偏函數(shù)還是很多 Scala 庫(kù)和 API 的重要組成部分。比如: Akka 中,actor 處理信息的方法就是通過(guò)偏函數(shù)來(lái)定義的。 因此,理解這一概念是非常重要的。

小結(jié)

在這一章中,我們學(xué)習(xí)了另一種定義匿名函數(shù)的方法:一系列的匹配樣例, 它用一種非常簡(jiǎn)潔的方式讓解構(gòu)數(shù)據(jù)成為可能。 而且,我們還深入到偏函數(shù)這個(gè)話題,用一個(gè)簡(jiǎn)單的例子展示了它的用處。

下一章,我們將深入的學(xué)習(xí)已經(jīng)出現(xiàn)過(guò)的 Option 類型,探索其存在的原因及其使用方式。

上一篇:路徑依賴類型下一篇:類型 Either