上一章總結(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]
一個很直觀的解決方案是使用 filter
和 map
函數(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ù)能幫我們解決 wordsWithoutOutliers
中的另外一個問題:在 wordsWithoutOutliers
中,我們首先過濾給定的序列,然后對剩下的元素進行映射,這種處理方式需要遍歷序列兩次。如果存在一種解法只需要遍歷一次,那不僅可以節(jié)省一些 CPU,還會使得代碼更簡潔,更具有可讀性。
Scala 集合的 API 有一個叫做 collect
的方法,對于 Seq[A]
,它有如下方法簽名:
def collect[B](pf: PartialFunction[A, B]): Seq[B]
這個方法將給定的 偏函數(shù)(partial function) 應(yīng)用到序列的每一個元素上,最后返回一個新的序列 - 偏函數(shù)做了 filter
和 map
要做的事情。
那偏函數(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ù)來定義的。因此,理解這一概念是非常重要的。
在這一章中,我們學習了另一種定義匿名函數(shù)的方法:一系列的匹配樣例,它用一種非常簡潔的方式讓解構(gòu)數(shù)據(jù)成為可能。而且,我們還深入到偏函數(shù)這個話題,用一個簡單的例子展示了它的用處。
下一章,我們將深入的學習已經(jīng)出現(xiàn)過的 Option
類型,探索其存在的原因及其使用方式。
更多建議: