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

2018-02-24 16:00 更新

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

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

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

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

songTitles.map(_.toLowerCase)

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

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

一個很直觀的解決方案是使用 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òu)出字段,那代碼可能更加美觀和可讀。

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

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

在兩個匿名函數(shù)里,我們只使用了一個匹配案例,因為我們知道這個樣例總是會匹配成功,要解構(gòu)的數(shù)據(jù)類型在編譯期就確定了,沒有會出錯的可能。這是模式匹配型匿名函數(shù)的一個非常常見的用法。

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

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

不過要注意,必須顯示的聲明值的類型,因為 Scala 編譯器無法從匿名函數(shù)中推導出其類型。

當然,也可以定義一系列更加復雜的的匹配案例。但是你必須的確保對于每一個可能的輸入,都會有一個樣例能夠匹配成功,不然,運行時會拋出 MatchError 。

偏函數(shù)

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

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

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

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

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

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

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

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

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

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

除了使用上面的這種方式,還可以顯示的擴展 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
  }
}

當然,前一種方法更為更為簡潔。

把定義好的 pf 傳遞給 map 函數(shù),能夠通過編譯期,但運行時會拋出 MatchError ,因為我們的偏函數(shù)并不是在所有輸入值上都有定義:

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

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

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

這個結(jié)果和我們最初的實現(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ì),比如說,它們可以被直接串聯(lián)起來,實現(xiàn)函數(shù)式的責任鏈模式(源自于面向?qū)ο蟪淌皆O(shè)計)。

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

小結(jié)

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

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

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號