Scala中的Partial Function

艺术地说,Scala中的Partial Function就是一个“残缺”的函数,就像一个严重偏科的学生,只对某些科目感兴趣,而对没有兴趣的内容弃若蔽履。Partial Function做不到以“偏”概全,因而需要将多个偏函数组合,最终才能达到全面覆盖的目的。所以这个Partial Function确实是一个“部分”的函数。

对比Function和Partial Function,更学术味的解释如下:

对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String 的函数可以接收任意Int值,并返回一个字符串。

对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有Int值为输入。

在Scala中,所有偏函数的类型皆被定义为PartialFunction[-A, +B]类型,PartialFunction[-A, +B]又派生自Function1。由于它仅仅处理输入参数的部分分支,因而它通过isDefineAt()来判断输入值是否应该由当前偏函数进行处理。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)
}  

既然偏函数仅处理部分分支,自然可以与模式匹配结合起来。case语句从本质上讲就是PartialFunction的子类。当我们定义了如下值:

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

实际上就是创建了一个PartialFunction[Int, String]的子类,其中isDefineAt方法提供类似这样的实现:

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

当我们通过p(1)去调用该偏函数时,就相当于调用了Int => String函数的apply()方法,从而返回转换后的值“one”。如果传入的参数使得isDifineAt返回false,就会抛出MatchError异常。追本溯源,是因为这里对偏函数值的调用,实则是调用了AbstractPartialFunction的apply()方法(case语句相当于是继承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()方法内部调用了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
  }

这正是执行p(2)会抛出MatchError的由来。

为什么要用偏函数呢?以我个人愚见,还是一个重用粒度的问题。函数式的编程思想是以一种“演绎法”而非“归纳法”去寻求解决空间。也就是说,它并不是要去归纳问题然后分解问题并解决问题,而是看透问题本质,定义最原初的操作和组合规则,面对问题时,可以通过组合各种函数去解决问题,这也正是“组合子(combinator)”的含义。偏函数则更进一步,将函数求解空间中各个分支也分离出来,形成可以被组合的偏函数。

偏函数中最常见的组合方法为orElse、andThen与compose。orElse相当于一个或运算,如果通过它将多个偏函数组合起来,就相当于形成了多个case合成的模式匹配。倘若所有偏函数满足了输入值的所有分支,组合起来就形成一个函数了。例如写一个求绝对值的运算,就可以利用偏函数:

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只是重写了这两个方法。这意味着函数之间的组合可以使用andThen与compose,偏函数也可以。这两个方法的功能都是将多个(偏)函数组合起来形成一个新函数,只是组合的顺序不同,andThen是组合第一个,接着是第二个,依次类推;而compose则顺序相反。

利用andThen组合偏函数,设计本质接近Pipe-and-Filter模式,每个偏函数都可以理解为是一个Filter。因为要将这些偏函数组合起来形成一个管道,这就要求被组合的偏函数其输入值与输出值必须支持可串接,即上一个偏函数的输出值会作为下一个偏函数的输入值。对比orElse,则有所不同,orElse要求组合的所有偏函数必须是同样类型的偏函数定义,例如都是Int => String,或者String => CustomizedClass。

在PartialFunction中,andThen方法返回的是一个名为AndThen的偏函数:

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接收的参数为k: B => C,即函数类型而非偏函数类型。当然,由于偏函数继承自函数,它也可以组合偏函数。如果andThen组合了偏函数,则要求输入参数必须满足所有参与组合的偏函数,否则就会抛出MatchError错误。例如编写一个函数,要求将字符串中的数字替换为对应的英文单词,则可以实现为:

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

如果调用p("123"),返回结果为"onetwo3",但如果传入p("13"),由于p2偏函数的isDefineAt返回false,就会抛出MatchError错误。

偏函数可以用在很多场景。例如我们可以利用orElse之类的语义,编写DSL风格的代码,使其更加灵活且可读。《DSL in Action》一书中就是用了orElse来处理金融行业的需求:

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)

也可以有效地利用偏函数的开放性,使得API的调用者可以根据具体的需求场景传入自己的case语句。例如Twitter的Effective Scala给出的案例:

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()方法也是一个偏函数:

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

由于偏函数继承自函数,因而,如果一个方法要求接收函数,那么它也可以接收偏函数。例如我们常常使用的map、filter等方法,就可以接收偏函数:

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的编码风格建议:

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

从本质上讲,假设这个list的类型为List[Option[String]],则前者传给map的其实是一个形如Option[String] => String的函数,后者则通过case语句创建了PartialFunction[Option[String], String]的实例传递给了map。

2015-08-17 09:22156Scala