藝術(shù)地說闰靴,Scala中的Partial Function就是一個“殘缺”的函數(shù),就像一個嚴(yán)重偏科的學(xué)生捧灰,只對某些科目感興趣淆九,而對沒有興趣的內(nèi)容棄若蔽履。Partial Function做不到以“偏”概全毛俏,因而需要將多個偏函數(shù)組合炭庙,最終才能達(dá)到全面覆蓋的目的。所以這個Partial Function確實是一個“部分”的函數(shù)煌寇。
對比Function和Partial Function焕蹄,更學(xué)術(shù)味的解釋如下:
- 對給定的輸入?yún)?shù)類型,函數(shù)可接受該類型的任何值阀溶。換句話說腻脏,一個(Int) => String 的函數(shù)可以接收任意Int值,并返回一個字符串银锻。
- 對給定的輸入?yún)?shù)類型永品,偏函數(shù)只能接受該類型的某些特定的值。一個定義為(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" }
實際上就是創(chuàng)建了一個PartialFunction[Int, String]的子類乎芳,其中isDefineAt方法提供類似這樣的實現(xiàn):
def isDefineAt(x: Int):Boolean = x == 1
當(dāng)我們通過p(1)去調(diào)用該偏函數(shù)時遵蚜,就相當(dāng)于調(diào)用了Int => String函數(shù)的apply()方法帖池,從而返回轉(zhuǎn)換后的值“one”。如果傳入的參數(shù)使得isDifineAt返回false吭净,就會拋出MatchError異常睡汹。追本溯源,是因為這里對偏函數(shù)值的調(diào)用寂殉,實則是調(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,就會將x值傳遞給PartialFunction.empty友扰。這個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)會拋出MatchError的由來。
為什么要用偏函數(shù)呢村怪?以我個人愚見秽浇,還是一個重用粒度的問題。函數(shù)式的編程思想是以一種“演繹法”而非“歸納法”去尋求解決空間甚负。也就是說柬焕,它并不是要去歸納問題然后分解問題并解決問題,而是看透問題本質(zhì)梭域,定義最原初的操作和組合規(guī)則斑举,面對問題時,可以通過組合各種函數(shù)去解決問題病涨,這也正是“組合子(combinator)”的含義富玷。偏函數(shù)則更進(jìn)一步,將函數(shù)求解空間中各個分支也分離出來没宾,形成可以被組合的偏函數(shù)凌彬。
偏函數(shù)中最常見的組合方法為orElse、andThen與compose循衰。orElse相當(dāng)于一個或運算,如果通過它將多個偏函數(shù)組合起來褐澎,就相當(dāng)于形成了多個case合成的模式匹配会钝。倘若所有偏函數(shù)滿足了輸入值的所有分支,組合起來就形成一個函數(shù)了工三。例如寫一個求絕對值的運算迁酸,就可以利用偏函數(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組合時,還可以直接組合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卻不同,它們實則被定義在Function中掸读,PartialFunction只是重寫了這兩個方法串远。這意味著函數(shù)之間的組合可以使用andThen與compose宏多,偏函數(shù)也可以。這兩個方法的功能都是將多個(偏)函數(shù)組合起來形成一個新函數(shù)澡罚,只是組合的順序不同伸但,andThen是組合第一個,接著是第二個留搔,依次類推更胖;而compose則順序相反。
利用andThen組合偏函數(shù)隔显,設(shè)計本質(zhì)接近Pipe-and-Filter模式却妨,每個偏函數(shù)都可以理解為是一個Filter。因為要將這些偏函數(shù)組合起來形成一個管道括眠,這就要求被組合的偏函數(shù)其輸入值與輸出值必須支持可串接彪标,即上一個偏函數(shù)的輸出值會作為下一個偏函數(shù)的輸入值。對比orElse哺窄,則有所不同捐下,orElse要求組合的所有偏函數(shù)必須是同樣類型的偏函數(shù)定義,例如都是Int => String萌业,或者String => CustomizedClass坷襟。
在PartialFunction中,andThen方法返回的是一個名為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ù)衙四,否則就會拋出MatchError錯誤。例如編寫一個函數(shù)患亿,要求將字符串中的數(shù)字替換為對應(yīng)的英文單詞传蹈,則可以實現(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,就會拋出MatchError錯誤咙冗。
偏函數(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()方法也是一個偏函數(shù):
trait Actor {
type Receive = Actor.Receive
def receive: Actor.Receive
}
object Actor {
type Receive = PartialFunction[Any, Unit]
}
由于偏函數(shù)繼承自函數(shù)士骤,因而,如果一個方法要求接收函數(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中巨缘,給出了一個使用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è)這個list的類型為List[Option[String]]采呐,則前者傳給map的其實是一個形如Option[String] => String的函數(shù)若锁,后者則通過case語句創(chuàng)建了PartialFunction[Option[String], String]的實例傳遞給了map。