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

提取器

在 Coursera 上,想必你遇到過(guò)一個(gè)非常強(qiáng)大的語(yǔ)言特性: 模式匹配 。 它可以解綁一個(gè)給定的數(shù)據(jù)結(jié)構(gòu)。 這不是 Scala 所特有的,在其他出色的語(yǔ)言中,如 Haskell、Erlang,模式匹配也扮演著重要的角色。

模式匹配可以解構(gòu)各種數(shù)據(jù)結(jié)構(gòu),包括 列表 ,以及 樣例類 。 但只有這些數(shù)據(jù)結(jié)構(gòu)才能被解構(gòu)嗎,還是可以用某種方式擴(kuò)展其使用范圍? 而且,它實(shí)際是怎么工作的? 是不是有什么魔法在里面,得以寫(xiě)些類似下面的代碼?

 case class User(firstName: String, lastName: String, score: Int)
 def advance(xs: List[User]) = xs match {
   case User(_, _, score1) :: User(_, _, score2) :: _ => score1 - score2
   case _ => 0
 }

事實(shí)證明沒(méi)有什么魔法,這都?xì)w功于提取器 。

提取器使用最為廣泛的使用有著與 構(gòu)造器 相反的效果: 構(gòu)造器從給定的參數(shù)列表創(chuàng)建一個(gè)對(duì)象, 而提取器卻是從傳遞給它的對(duì)象中提取出構(gòu)造該對(duì)象的參數(shù)。 Scala 標(biāo)準(zhǔn)庫(kù)包含了一些預(yù)定義的提取器,我們會(huì)大致的了解一下它們。

樣例類非常特殊,Scala會(huì)自動(dòng)為其創(chuàng)建一個(gè) 伴生對(duì)象 : 一個(gè)包含了 applyunapply 方法的 單例對(duì)象apply 方法用來(lái)創(chuàng)建樣例類的實(shí)例,而 unapply 需要被伴生對(duì)象實(shí)現(xiàn),以使其成為提取器。

第一個(gè)提取器

unapply 方法可能不止有一種方法簽名, 不過(guò),我們從只有最簡(jiǎn)單的開(kāi)始,畢竟使用更廣泛的還是只有一種方法簽名的 unapply 。 假設(shè)要?jiǎng)?chuàng)建了一個(gè) User 特質(zhì),有兩個(gè)類繼承自它,并且包含一個(gè)字段:

trait User {
  def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User

我們想在各自的伴生對(duì)象中為 FreeUserPremiumUser 類實(shí)現(xiàn)提取器, 就像 Scala 為樣例類所做的一樣。 如果想讓樣例類只支持從給定對(duì)象中提取單個(gè)參數(shù),那 unapply 方法的簽名看起來(lái)應(yīng)該是這個(gè)樣子:

  def unapply(object: S): Option[T]

這個(gè)方法接受一個(gè)類型為 S 的對(duì)象,返回類型 TOptionT 就是要提取的參數(shù)類型。

在Scala中, Optionnull 值的安全替代。 以后會(huì)有一個(gè)單獨(dú)的章節(jié)來(lái)講述它,不過(guò)現(xiàn)在,只需要知道, unapply 方法要么返回 Some[T] (如果它能成功提取出參數(shù)),要么返回 NoneNone 表示參數(shù)不能被 unapply 具體實(shí)現(xiàn)中的任一提取規(guī)則所提取出。

下面的代碼是我們的提取器:

trait User {
  def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User
object FreeUser {
  def unapply(user: FreeUser): Option[String] = Some(user.name)
}
object PremiumUser {
  def unapply(user: PremiumUser): Option[String] = Some(user.name)
}

現(xiàn)在,可以在REPL中使用它:

scala> FreeUser.unapply(new FreeUser("Daniel"))
res0: Option[String] = Some(Daniel)

如果調(diào)用返回的結(jié)果是 Some[T] ,說(shuō)明提取模式匹配成功,如果是 None ,說(shuō)明模式不匹配。

一般不會(huì)直接調(diào)用它,因?yàn)橛糜谔崛∑髂J綍r(shí),Scala 會(huì)隱式的調(diào)用提取器的 unapply 方法。

  val user: User = new PremiumUser("Daniel")
  user match {
    case FreeUser(name) => "Hello" + name
    case PremiumUser(name) => "Welcome back, dear" + name
  }

你會(huì)發(fā)現(xiàn),兩個(gè)提取器絕不會(huì)都返回 None 。 這個(gè)例子展示的提取器要比之前所見(jiàn)的更有意義。 如果你有一個(gè)類型不確定的對(duì)象,你可以同時(shí)檢查其類型并解構(gòu)。

這個(gè)例子里, FreeUser 模式并不會(huì)匹配,因?yàn)樗邮艿念愋秃臀覀儌鬟f給它的不一樣。 這樣一來(lái), user 對(duì)象就會(huì)被傳遞給第二個(gè)模式,也就是 PremiumUser 伴生對(duì)象的 unapply 方法。 而這個(gè)模式會(huì)匹配成功,從而返回值就被綁定到 name 參數(shù)上。

在接下來(lái)的文章里,我們會(huì)看到一個(gè)并不總是返回 Some[T] 的提取器的例子。

提取多個(gè)值

現(xiàn)在,假設(shè)類有多個(gè)字段:

trait User {
  def name: String
  def score: Int
}
class FreeUser(
  val name: String,
  val score: Int,
  val upgradeProbability: Double
) extends User
class PremiumUser(
  val name: String,
  val score: Int
) extends User

如果提取器想解構(gòu)出多個(gè)參數(shù),那它的 unapply 方法應(yīng)該有這樣的簽名:

def unapply(object: S): Option[(T1, ..., T2)]

這個(gè)方法接受類型為 S 的對(duì)象,返回類型參數(shù)為 TupleNOption 實(shí)例, TupleN 中的 N 是要提取的參數(shù)個(gè)數(shù)。

修改類之后,提取器也要做相應(yīng)的修改:

trait User {
  def name: String
  def score: Int
}
class FreeUser(
  val name: String,
  val score: Int,
  val upgradeProbability: Double
) extends User
class PremiumUser(
  val name: String,
  val score: Int
) extends User
object FreeUser {
  def unapply(user: FreeUser): Option[(String, Int, Double)] =
    Some((user.name, user.score, user.upgradeProbability))
}
object PremiumUser {
  def unapply(user: PremiumUser): Option[(String, Int)] =
    Some((user.name, user.score))
}

現(xiàn)在可以拿它來(lái)做模式匹配了:

val user: User = new FreeUser("Daniel", 3000, 0.7d)
user match {
  case FreeUser(name, _, p) =>
    if (p > 0.75) "$name, what can we do for you today?"
    else "Hello $name"
  case PremiumUser(name, _) =>
    "Welcome back, dear $name"
}

布爾提取器

有些時(shí)候,進(jìn)行模式匹配并不是為了提取參數(shù),而是為了檢查其是否匹配。 這種情況下,第三種 unapply 方法簽名(也是最后一種)就有用了, 這個(gè)方法接受 S 類型的對(duì)象,返回一個(gè)布爾值:

def unapply(object: S): Boolean

使用的時(shí)候,如果這個(gè)提取器返回 true ,模式會(huì)匹配成功, 否則,Scala 會(huì)嘗試拿 object 匹配下一個(gè)模式。

上一個(gè)例子存在一些邏輯代碼,用來(lái)檢查一個(gè)免費(fèi)用戶有沒(méi)有可能被說(shuō)服去升級(jí)他的賬戶。 其實(shí)可以把這個(gè)邏輯放在一個(gè)單獨(dú)的提取器中:

object premiumCandidate {
  def unapply(user: FreeUser): Boolean = user.upgradeProbability > 0.75
}

你會(huì)發(fā)現(xiàn),提取器不一定非要在這個(gè)類的伴生對(duì)象中定義。 正如其定義一樣,這個(gè)提取器的使用方法也很簡(jiǎn)單:

val user: User = new FreeUser("Daniel", 2500, 0.8d)
user match {
  case freeUser @ premiumCandidate() => initiateSpamProgram(freeUser)
  case _ => sendRegularNewsletter(user)
}

使用的時(shí)候,只需要把一個(gè)空的參數(shù)列表傳遞給提取器,因?yàn)樗⒉徽娴男枰崛?shù)據(jù),自然也沒(méi)必要綁定變量。

這個(gè)例子有一個(gè)看起來(lái)比較奇怪的地方: 我假設(shè)存在一個(gè)空想的 initiateSpamProgram 函數(shù),其接受一個(gè) FreeUser 對(duì)象作為參數(shù)。 模式可以與任何一種 User 類型的實(shí)例進(jìn)行匹配,但 initiateSpamProgram 不行, 只有將實(shí)例強(qiáng)制轉(zhuǎn)換為 FreeUser 類型, initiateSpamProgram 才能接受。

因?yàn)槿绱?,Scala 的模式匹配也允許將提取器匹配成功的實(shí)例綁定到一個(gè)變量上, 這個(gè)變量有著與提取器所接受的對(duì)象相同的類型。這通過(guò) @ 操作符實(shí)現(xiàn)。 premiumCandidate 接受 FreeUser 對(duì)象,因此,變量 freeUser 的類型也就是 FreeUser 。

布爾提取器的使用并沒(méi)有那么頻繁(就我自己的情況來(lái)說(shuō)),但知道它存在也是很好的, 或遲或早,你會(huì)遇到一個(gè)使用布爾提取器的場(chǎng)景。

中綴表達(dá)方式

解構(gòu)列表、流的方法與創(chuàng)建它們的方法類似,都是使用 cons 操作符: :: 、 #:: ,比如:

val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
  case first #:: second #:: _ => first - second
  case _ => -1
}

你可能會(huì)對(duì)這種做法產(chǎn)生困惑。 除了我們已經(jīng)見(jiàn)過(guò)的提取器用法,Scala 還允許以中綴方式來(lái)使用提取器。 所以,我們可以寫(xiě)成 e(p1, p2) ,也可以寫(xiě)成 p1 e p2 , 其中 e 是提取器, p1 、 p2 是要提取的參數(shù)。

同樣,中綴操作方式的 head #:: tail 可以被寫(xiě)成 #::(head, tail) , 提取器 PremiumUser 可以這樣使用: name PremiumUser score 。 當(dāng)然,這樣做并沒(méi)有什么實(shí)踐意義。 一般來(lái)說(shuō),只有當(dāng)一個(gè)提取器看起來(lái)真的像操作符,才推薦以中綴操作方式來(lái)使用它。 所以,列表和流的 cons 操作符一般使用中綴表達(dá),而 PreimumUser 則不用。

進(jìn)一步看流提取器

盡管 #:: 提取器在模式匹配中的使用并沒(méi)有什么特殊的, 但是,為了更好的理解上面的代碼,還是進(jìn)一步來(lái)分析一下。 而且,這是一個(gè)很好的例子,根據(jù)要匹配的數(shù)據(jù)結(jié)構(gòu)的狀態(tài),提取器很可能返回 None

如下是 Scala 2.9.2 源代碼中完整的 #:: 提取器代碼:

object #:: {
  def unapply[A](xs: Stream[A]): Option[(A, Stream[A]) =
    if (xs.isEmpty) None
    else Some((xs.head, xs.tail))
}

如果給定的流是空的,提取器就直接返回 None 。 因此, case head #:: tail 就不會(huì)匹配任何空的流。 否則,就會(huì)返回一個(gè) Tuple2 ,其第一個(gè)元素是流的頭,第二個(gè)元素是流的尾,尾本身又是一個(gè)流。 這樣, case head #:: tail 就會(huì)匹配有一個(gè)或多個(gè)元素的流。 如果只有一個(gè)元素, tail 就會(huì)被綁定成空流。

為了理解流提取器是怎么在模式匹配中工作的,重寫(xiě)上面的例子,把它從中綴寫(xiě)法轉(zhuǎn)成普通的提取器模式寫(xiě)法:

val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
  case #::(first, #::(second, _)) => first - second
  case _ => -1
}

首先為傳遞給模式匹配的初始流 xs 調(diào)用提取器。 由于提取器返回 Some(xs.head, xs.tail) ,從而 first 會(huì)綁定成 58, xs 的尾會(huì)繼續(xù)傳遞給提取器,提取器再一次被調(diào)用,返回首和尾, second 就被綁定成 43 , 而尾就綁定到通配符 _ ,被直接扔掉了。

使用提取器

那到底該在什么時(shí)候使用、怎么使用自定義的提取器呢?尤其考慮到,使用樣例類就能自動(dòng)獲得可用的提取器。

一些人指出,使用樣例類、對(duì)樣例類進(jìn)行模式匹配打破了封裝, 耦合了匹配數(shù)據(jù)和其具體實(shí)現(xiàn)的方式,這種批評(píng)通常是從面向?qū)ο蟮慕嵌瘸霭l(fā)的。 如果想用 Scala 進(jìn)行函數(shù)式編程,將樣例類當(dāng)作只包含純數(shù)據(jù)(不包含行為)的 代數(shù)數(shù)據(jù)類型 ,那它非常適合。

通常,只有當(dāng)從無(wú)法掌控的類型中提取數(shù)據(jù),或者是需要其他進(jìn)行模式匹配的方法時(shí),才需要實(shí)現(xiàn)自己的提取器。

提取器的一種常見(jiàn)用法是從字符串中提取出有意義的值, 作為練習(xí),想一想如何實(shí)現(xiàn) URLExtractor 以匹配代表 URL 的字符串。

小結(jié)

在這本書(shū)的第一章中,我們學(xué)習(xí)了 Scala 模式匹配背后的提取器, 學(xué)會(huì)了如何實(shí)現(xiàn)自己的提取器,及其在模式中的使用是如何和實(shí)現(xiàn)聯(lián)系在一起的。 但是這并不是提取器的全部,下一章,將會(huì)學(xué)習(xí)如何實(shí)現(xiàn)可提取可變個(gè)數(shù)參數(shù)的提取器。