上一章總結了模式在 Scala 中的幾種用法,最后提到了匿名函數。這一章,我們具體的去學習如何在匿名函數中使用模式。
如果你參與過 Coursera 上的 [那門 Scala 課程](https://www.coursera.org/course/progfun) ,或者寫過 Scala 代碼,那很可能你已經熟悉匿名函數。比如說,將一組歌名轉換成小寫格式,你可能會定義一個匿名函數傳遞給 `map` 方法:
~~~
val songTitles = List("The White Hare", "Childe the Hunter", "Take no Rogues")
songTitles.map(t => t.toLowerCase)
~~~
或者,利用 Scala 的 _占位符語法(placeholder syntax)_ 得到更加簡短的代碼:
~~~
songTitles.map(_.toLowerCase)
~~~
目前為止,一切都很順利。不過,讓我們來看一個稍微有些區別的例子:假設有一個由二元組組成的序列,每個元組包含一個單詞,以及對應的詞頻,我們的目標就是去除詞頻太高或者太低的單詞,只保留中間地帶的。需要寫出這樣一個函數:
~~~
wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String]
~~~
一個很直觀的解決方案是使用 `filter` 和 `map` 函數:
~~~
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")
~~~
這個解法有幾個問題。首先,訪問元組字段的代碼不好看,如果我們可以直接解構出字段,那代碼可能更加美觀和可讀。
幸好,Scala 提供了另外一種寫匿名函數的方式:_模式匹配形式的匿名函數_,它是由一系列模式匹配樣例組成的,正如模式匹配表達式那樣,不過沒有 `match` 。下面是重寫后的代碼:
~~~
def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
wordFrequencies.filter { case (_, f) `> f > 3 && f < 25 } map { case (w, _) `> w }
~~~
在兩個匿名函數里,我們只使用了一個匹配案例,因為我們知道這個樣例總是會匹配成功,要解構的數據類型在編譯期就確定了,沒有會出錯的可能。這是模式匹配型匿名函數的一個非常常見的用法。
如果把這些匿名函數賦給其他值,你也會看到它們有著正確的類型:
~~~
val predicate: (String, Int) `> Boolean ` { case (_, f) => f > 3 && f < 25 }
val transformFn: (String, Int) `> String ` { case (w, _) => w }
~~~
> 不過要注意,必須顯示的聲明值的類型,因為 Scala 編譯器無法從匿名函數中推導出其類型。
當然,也可以定義一系列更加復雜的的匹配案例。但是你必須的確保對于每一個可能的輸入,都會有一個樣例能夠匹配成功,不然,運行時會拋出 `MatchError` 。
### 偏函數
有時候可能會定義一個只處理特定輸入的函數。這樣的一種函數能幫我們解決 `wordsWithoutOutliers` 中的另外一個問題:在 `wordsWithoutOutliers` 中,我們首先過濾給定的序列,然后對剩下的元素進行映射,這種處理方式需要遍歷序列兩次。如果存在一種解法只需要遍歷一次,那不僅可以節省一些 CPU,還會使得代碼更簡潔,更具有可讀性。
Scala 集合的 API 有一個叫做 `collect` 的方法,對于 `Seq[A]` ,它有如下方法簽名:
~~~
def collect[B](pf: PartialFunction[A, B]): Seq[B]
~~~
這個方法將給定的 _偏函數(partial function)_ 應用到序列的每一個元素上,最后返回一個新的序列 - 偏函數做了 `filter` 和 `map` 要做的事情。
那偏函數到底是什么呢?概括來說,偏函數是一個一元函數,它只在部分輸入上有定義,并且允許使用者去檢查其在一個給定的輸入上是否有定義。為此,特質 `PartialFunction` 提供了一個 `isDefinedAt` 方法。事實上,類型 `PartialFunction[-A, +B]` 擴展了類型 `(A) => B`(一元函數,也可以寫成 `Function1[A, B]` )。模式匹配型的匿名函數的類型就是 `PartialFunction` 。
依據繼承關系,將一個模式匹配型的匿名函數傳遞給接受一元函數的方法(如:`map`、`filter`)是沒有問題的,只要這個匿名函數對于所有可能的輸入都有定義。
不過 `collect` 方法接受的函數只能是 `PartialFunction[A, B]` 類型的。對于序列中的每一個元素,首先檢查偏函數在其上面是否有定義,如果沒有定義,那這個元素就直接被忽略掉,否則,就將偏函數應用到這個元素上,返回的結果加入結果集。
現在,我們來重構 `wordsWithoutOutliers` ,首先定義需要的偏函數:
~~~
val pf: PartialFunction[(String, Int), String] = {
case (word, freq) if freq > 3 && freq < 25 => word
}
~~~
我們為這個案例加入了 _守衛語句_,不在區間里的元素就沒有定義。
除了使用上面的這種方式,還可以顯示的擴展 `PartialFunction` 特質:
~~~
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` 函數,能夠通過編譯期,但運行時會拋出 `MatchError` ,因為我們的偏函數并不是在所有輸入值上都有定義:
~~~
wordFrequencies.map(pf) // will throw a MatchError
~~~
不過,把它傳遞給 `collect` 函數就能得到想要的結果:
~~~
wordFrequencies.collect(pf) // List("habitual", "homely", "society")
~~~
這個結果和我們最初的實現所得到的結果是一樣的,因此我們可以重寫 `wordsWithoutOutliers`:
~~~
def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
wordFrequencies.collect { case (word, freq) if freq > 3 && freq < 25 => word }
~~~
偏函數還有其他一些有用的性質,比如說,它們可以被直接串聯起來,實現函數式的[責任鏈模式](http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern)(源自于面向對象程式設計)。
偏函數還是很多 Scala 庫和 API 的重要組成部分。比如:[Akka](http://akka.io) 中,actor 處理信息的方法就是通過偏函數來定義的。因此,理解這一概念是非常重要的。
### 小結
在這一章中,我們學習了另一種定義匿名函數的方法:一系列的匹配樣例,它用一種非常簡潔的方式讓解構數據成為可能。而且,我們還深入到偏函數這個話題,用一個簡單的例子展示了它的用處。
下一章,我們將深入的學習已經出現過的 `Option` 類型,探索其存在的原因及其使用方式。