|
|
51CTO旗下网站
|
|
移动端
创建专栏

一文读懂Scala中的偏函数

艺术地说,Scala中的Partial Function就是一个“残缺”的函数,Partial Function做不到以“偏”概全,因而需要将多个偏函数组合,最终才能达到全面覆盖的目的。

作者:张逸|2017-03-07 15:13

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

一文读懂Scala中的偏函数

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

  • 对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String 的函数可以接收任意Int值,并返回一个字符串。
  • 对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有Int值为输入。

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

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

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

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

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

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

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

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

apply()方法内部调用了PartialFunction的applyOrElse()方法。若isDefineAt(x)返回为false,就会将x值传递给PartialFunction.empty。这个empty等于类型为PartialFunction[Any, Nothong]的值empty_pf,定义如下:

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

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

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

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

  1. val positiveNumber:PartialFunction[Int, Int] = {  
  2.     case x if x > 0 => x  
  3. val zero:PartialFunction[Int, Int] = {  
  4.     case x if x == 0 => 0  
  5. val negativeNumber:PartialFunction[Int, Int] = {  
  6.     case x if x < 00 => -x  
  7. def abs(x: Int): Int = { 
  8.     (positiveNumber orElse zero orElse negativeNumber)(x) 

利用orElse组合时,还可以直接组合case语句,例如:

  1. val pf: PartialFunction[Int, String] = { 
  2.   case i if i%2 == 0 => "even" 
  3. 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的偏函数:

  1. trait PartialFunction[-A, +B] extends (A => B) { 
  2.   override def andThen[C](k: B => C): PartialFunction[A, C] = 
  3.     new AndThen[A, B, C] (this, k) 
  4. object PartialFunction { 
  5.   private class AndThen[-A, B, +C] (pf: PartialFunction[A, B], k: B => C) extends PartialFunction[A, C] { 
  6.     def isDefinedAt(x: A) = pf.isDefinedAt(x) 
  7.     def apply(x: A): C = k(pf(x)) 
  8.     override def applyOrElse[A1 <: A, C1 >: C](x: A1, default: A1 => C1): C1 = { 
  9.       val z = pf.applyOrElse(x, checkFallback[B]) 
  10.       if (!fallbackOccurred(z)) k(z) else default(x) 
  11.     } 
  12.   } 

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

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

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

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

  1. val forHKG:PartialFunction[Market, List[TaxFee]] = ... 
  2. val forSGP:PartialFunction[Market, List[TaxFee]] = ... 
  3. val forAll:PartialFunction[Market, List[TaxFee]] = ... 
  4.  
  5. def forTrade(trade: Trade): List[TaxFee] =  
  6.     (forHKG orElse forSGP orElse forAll)(trade.market) 

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

  1. trait Publisher[T] { 
  2.   def subscribe(f: PartialFunction[T, Unit]) 
  3. val publisher: Publisher[Int] = ... 
  4.  
  5. publisher.subscribe { 
  6.   case i if isPrime(i) => println("found prime", i) 
  7.   case i if i%2 == 0 => count += 2 
  8.   /* ignore the rest */ 

定义在AKKA的Actor中的receive()方法也是一个偏函数:

  1. trait Actor { 
  2.     type Receive = Actor.Receive 
  3.     def receive: Actor.Receive 
  4. object Actor { 
  5.   type Receive = PartialFunction[Any, Unit] 

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

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

在Twitter的Effetive Scala中,给出了一个使用map的编码风格建议:

  1. //avoid 
  2. list map { item => 
  3.   item match { 
  4.     case Some(x) => x 
  5.     case None => default 
  6.   } 
  7. //recommend 
  8. list map { 
  9.   case Some(x) => x 
  10.   case None => default 

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

【本文为51CTO专栏作者“张逸”原创稿件,转载请联系原作者】

戳这里,看该作者更多好文

【编辑推荐】

  1. 深度学习入门课程学习笔记03 损失函数
  2. 常见缓冲区溢出函数
  3. C++中的虚函数重载
  4. 作为Scala语法糖的设计模式
  5. 基于Scala Trait的设计模式
【责任编辑:IT疯 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢