Scala中的Partial Function

藝術(shù)地說廉油,Scala中的Partial Function就是一個(gè)“殘缺”的函數(shù),就像一個(gè)嚴(yán)重偏科的學(xué)生,只對某些科目感興趣眨猎,而對沒有興趣的內(nèi)容棄若蔽履夕凝。Partial Function做不到以“偏”概全,因而需要將多個(gè)偏函數(shù)組合须鼎,最終才能達(dá)到全面覆蓋的目的姓赤。所以這個(gè)Partial Function確實(shí)是一個(gè)“部分”的函數(shù)。

對比Function和Partial Function,更學(xué)術(shù)味的解釋如下:

對給定的輸入?yún)?shù)類型毕谴,函數(shù)可接受該類型的任何值。換句話說,一個(gè)(Int) => String 的函數(shù)可以接收任意Int值衷旅,并返回一個(gè)字符串操软。

對給定的輸入?yún)?shù)類型,偏函數(shù)只能接受該類型的某些特定的值。一個(gè)定義為(Int) => String 的偏函數(shù)可能不能接受所有Int值為輸入业崖。

在Scala中撮抓,所有偏函數(shù)的類型皆被定義為PartialFunction[-A, +B]類型站超,PartialFunction[-A, +B]又派生自Function1。由于它僅僅處理輸入?yún)?shù)的部分分支施掏,因而它通過isDefineAt()來判斷輸入值是否應(yīng)該由當(dāng)前偏函數(shù)進(jìn)行處理素挽。PartialFunction的定義如下所示:

trait PartialFunction[-A, +B] extends (A => B) { self =>
  import PartialFunction._
  def isDefinedAt(x: A): Boolean
  def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 =
    if (isDefinedAt(x)) apply(x) else default(x)
}  

既然偏函數(shù)僅處理部分分支撰糠,自然可以與模式匹配結(jié)合起來术辐。case語句從本質(zhì)上講就是PartialFunction的子類。當(dāng)我們定義了如下值:

val p:PartialFunction[Int, String] = { case 1 => "One" }

實(shí)際上就是創(chuàng)建了一個(gè)PartialFunction[Int, String]的子類,其中isDefineAt方法提供類似這樣的實(shí)現(xiàn):

def isDefineAt(x: Int):Boolean = x == 1

當(dāng)我們通過p(1)去調(diào)用該偏函數(shù)時(shí),就相當(dāng)于調(diào)用了Int => String函數(shù)的apply()方法,從而返回轉(zhuǎn)換后的值“one”仿荆。如果傳入的參數(shù)使得isDifineAt返回false互艾,就會(huì)拋出MatchError異常甲雅。追本溯源弛姜,是因?yàn)檫@里對偏函數(shù)值的調(diào)用绝页,實(shí)則是調(diào)用了AbstractPartialFunction的apply()方法(case語句相當(dāng)于是繼承AbstractPartialFunction的子類):

abstract class AbstractPartialFunction[@specialized(scala.Int, scala.Long, scala.Float, scala.Double, scala.AnyRef) -T1, @specialized(scala.Unit, scala.Boolean, scala.Int, scala.Float, scala.Long, scala.Double, scala.AnyRef) +R] extends Function1[T1, R] with PartialFunction[T1, R] { self =>
    def apply(x: T1): R = applyOrElse(x, PartialFunction.empty)
}

apply()方法內(nèi)部調(diào)用了PartialFunction的applyOrElse()方法屈芜。若isDefineAt(x)返回為false,就會(huì)將x值傳遞給PartialFunction.empty焦蘑。這個(gè)empty等于類型為PartialFunction[Any, Nothong]的值empty_pf,定義如下:

  private[this] val empty_pf: PartialFunction[Any, Nothing] = new PartialFunction[Any, Nothing] {
    def isDefinedAt(x: Any) = false
    def apply(x: Any) = throw new MatchError(x)
    override def orElse[A1, B1](that: PartialFunction[A1, B1]) = that
    override def andThen[C](k: Nothing => C) = this
    override val lift = (x: Any) => None
    override def runWith[U](action: Nothing => U) = constFalse
  }

這正是執(zhí)行p(2)會(huì)拋出MatchError的由來腋腮。

為什么要用偏函數(shù)呢袜刷?以我個(gè)人愚見梢莽,還是一個(gè)重用粒度的問題。函數(shù)式的編程思想是以一種“演繹法”而非“歸納法”去尋求解決空間。也就是說,它并不是要去歸納問題然后分解問題并解決問題颂斜,而是看透問題本質(zhì)司蔬,定義最原初的操作和組合規(guī)則,面對問題時(shí),可以通過組合各種函數(shù)去解決問題跛十,這也正是“組合子(combinator)”的含義。偏函數(shù)則更進(jìn)一步憋飞,將函數(shù)求解空間中各個(gè)分支也分離出來唁盏,形成可以被組合的偏函數(shù)。

偏函數(shù)中最常見的組合方法為orElse、andThen與compose眨补。orElse相當(dāng)于一個(gè)或運(yùn)算崎弃,如果通過它將多個(gè)偏函數(shù)組合起來,就相當(dāng)于形成了多個(gè)case合成的模式匹配酌伊。倘若所有偏函數(shù)滿足了輸入值的所有分支驴娃,組合起來就形成一個(gè)函數(shù)了。例如寫一個(gè)求絕對值的運(yùn)算,就可以利用偏函數(shù):

val positiveNumber:PartialFunction[Int, Int] = { case x if x > 0 => x }
val zero:PartialFunction[Int, Int] = { case x if x == 0 => 0 }
val negativeNumber:PartialFunction[Int, Int] = { case x if x < 0 => -x }

def abs(x: Int): Int = {
    (positiveNumber orElse zero orElse negativeNumber)(x)
} 

利用orElse組合時(shí)模叙,還可以直接組合case語句故觅,例如:

val pf: PartialFunction[Int, String] = {
  case i if i%2 == 0 => "even"
}
val tf: (Int => String) = pf orElse { case _ => "odd" }

orElse被定義在PartialFunction類型中贯溅,而andThen與compose卻不同熙含,它們實(shí)則被定義在Function中,PartialFunction只是重寫了這兩個(gè)方法。這意味著函數(shù)之間的組合可以使用andThen與compose夜牡,偏函數(shù)也可以。這兩個(gè)方法的功能都是將多個(gè)(偏)函數(shù)組合起來形成一個(gè)新函數(shù)猴娩,只是組合的順序不同矛双,andThen是組合第一個(gè)无埃,接著是第二個(gè)侦镇,依次類推;而compose則順序相反。

利用andThen組合偏函數(shù),設(shè)計(jì)本質(zhì)接近Pipe-and-Filter模式皂冰,每個(gè)偏函數(shù)都可以理解為是一個(gè)Filter舶胀。因?yàn)橐獙⑦@些偏函數(shù)組合起來形成一個(gè)管道嚣伐,這就要求被組合的偏函數(shù)其輸入值與輸出值必須支持可串接,即上一個(gè)偏函數(shù)的輸出值會(huì)作為下一個(gè)偏函數(shù)的輸入值耿导。對比orElse,則有所不同芥驳,orElse要求組合的所有偏函數(shù)必須是同樣類型的偏函數(shù)定義丽猬,例如都是Int => String,或者String => CustomizedClass。

在PartialFunction中碍舍,andThen方法返回的是一個(gè)名為AndThen的偏函數(shù):

trait PartialFunction[-A, +B] extends (A => B) {
  override def andThen[C](k: B => C): PartialFunction[A, C] =
    new AndThen[A, B, C] (this, k)
}
object PartialFunction {
  private class AndThen[-A, B, +C] (pf: PartialFunction[A, B], k: B => C) extends PartialFunction[A, C] {
    def isDefinedAt(x: A) = pf.isDefinedAt(x)

    def apply(x: A): C = k(pf(x))

    override def applyOrElse[A1 <: A, C1 >: C](x: A1, default: A1 => C1): C1 = {
      val z = pf.applyOrElse(x, checkFallback[B])
      if (!fallbackOccurred(z)) k(z) else default(x)
    }
  }
}  

注意看捧书,andThen接收的參數(shù)為k: B => C爆哑,即函數(shù)類型而非偏函數(shù)類型色冀。當(dāng)然屯换,由于偏函數(shù)繼承自函數(shù)嘉抓,它也可以組合偏函數(shù)幕屹。如果andThen組合了偏函數(shù),則要求輸入?yún)?shù)必須滿足所有參與組合的偏函數(shù)鸥跟,否則就會(huì)拋出MatchError錯(cuò)誤拟淮。例如編寫一個(gè)函數(shù),要求將字符串中的數(shù)字替換為對應(yīng)的英文單詞,則可以實(shí)現(xiàn)為:

val p1:PartialFunction[String, String] = { case s if s.contains("1") => s.replace("1", "one") }
val p2:PartialFunction[String, String] = { case s if s.contains("2") => s.replace("2", "two") }
val p = p1 andThen p2

如果調(diào)用p("123"),返回結(jié)果為"onetwo3"爬虱,但如果傳入p("13")继蜡,由于p2偏函數(shù)的isDefineAt返回false,就會(huì)拋出MatchError錯(cuò)誤。

偏函數(shù)可以用在很多場景耕皮。例如我們可以利用orElse之類的語義罚拟,編寫DSL風(fēng)格的代碼,使其更加靈活且可讀阻逮∧盘颍《DSL in Action》一書中就是用了orElse來處理金融行業(yè)的需求:

val forHKG:PartialFunction[Market, List[TaxFee]] = ...
val forSGP:PartialFunction[Market, List[TaxFee]] = ...
val forAll:PartialFunction[Market, List[TaxFee]] = ...

def forTrade(trade: Trade): List[TaxFee] = 
    (forHKG orElse forSGP orElse forAll)(trade.market)

也可以有效地利用偏函數(shù)的開放性流炕,使得API的調(diào)用者可以根據(jù)具體的需求場景傳入自己的case語句渠欺。例如[Twitter的Effective Scala](http://twitter.github.io/effectivescala/#Functional programming-Partial functions)給出的案例:

trait Publisher[T] {
  def subscribe(f: PartialFunction[T, Unit])
}

val publisher: Publisher[Int] = ...
publisher.subscribe {
  case i if isPrime(i) => println("found prime", i)
  case i if i%2 == 0 => count += 2
  /* ignore the rest */
}

定義在AKKA的Actor中的receive()方法也是一個(gè)偏函數(shù):

trait Actor {
    type Receive = Actor.Receive
    def receive: Actor.Receive
}
object Actor {
  type Receive = PartialFunction[Any, Unit]
}

由于偏函數(shù)繼承自函數(shù)乳丰,因而汞斧,如果一個(gè)方法要求接收函數(shù)仲义,那么它也可以接收偏函數(shù)。例如我們常常使用的map暂刘、filter等方法森缠,就可以接收偏函數(shù):

val sample = 1 to 10
sample map {
    case x if x % 2 == 0 => x + " is even"
    case x if x % 2 == 1 => x + " is odd"
}

Twitter的Effetive Scala中,給出了一個(gè)使用map的編碼風(fēng)格建議:

//avoid
list map { item =>
  item match {
    case Some(x) => x
    case None => default
  }
}
//recommend
list map {
  case Some(x) => x
  case None => default
}

從本質(zhì)上講,假設(shè)這個(gè)list的類型為List[Option[String]],則前者傳給map的其實(shí)是一個(gè)形如Option[String] => String的函數(shù)树瞭,后者則通過case語句創(chuàng)建了PartialFunction[Option[String], String]的實(shí)例傳遞給了map访敌。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市陈莽,隨后出現(xiàn)的幾起案子独柑,更是在濱河造成了極大的恐慌索绪,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異曙博,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來藕各,“玉大人作彤,你說我怎么就攤上這事代咸。” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵蓝翰,是天一觀的道長。 經(jīng)常有香客問我愕鼓,道長,這世上最難降的妖魔是什么磺送? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮司顿,結(jié)果婚禮上估脆,老公的妹妹穿的比我還像新娘。我一直安慰自己厌衔,他們只是感情好页徐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豌熄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪压鉴。 梳的紋絲不亂的頭發(fā)上击蹲,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天类咧,我揣著相機(jī)與錄音血巍,去河邊找鬼。 笑死鲫凶,一個(gè)胖子當(dāng)著我的面吹牛昼钻,可吹牛的內(nèi)容都是我干的仅财。 我是一名探鬼主播亿眠,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼倦始!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铸本,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤锡足,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蝎亚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磨隘,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡设预,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了魏烫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呐赡。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡管闷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤像寒,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響仰禀,放射性物質(zhì)發(fā)生泄漏审胸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一堰氓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧软免,春花似錦、人聲如沸胚委。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跪另。三九已至擦盾,卻和暖如春辽故,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狰晚。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工碘裕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雷滋,地道東北人澳泵。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓维苔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親饿序。 傳聞我的和親對象是個(gè)殘疾皇子乱凿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 藝術(shù)地說,Scala中的Partial Function就是一個(gè)“殘缺”的函數(shù),就像一個(gè)嚴(yán)重偏科的學(xué)生姥闪,只對某些科...
    _張逸_閱讀 835評論 4 3
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器顿天,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • title: "Swift 中枚舉高級用法及實(shí)踐"date: 2015-11-20tags: [APPVENTUR...
    guoshengboy閱讀 2,559評論 0 2
  • 86.復(fù)合 Cases 共享相同代碼塊的多個(gè)switch 分支 分支可以合并, 寫在分支后用逗號分開懂从。如果任何模式...
    無灃閱讀 1,345評論 1 5
  • 人這一輩子窍育,說長也長,說短也短乞娄。 一輩子接著一輩子追迟,活著為了啥瓶逃? 經(jīng)常會(huì)這樣的迷茫带猴。 求解。
    FeelLiang閱讀 140評論 0 0