从艺术上讲,Scala中的PartialFunction是一个“不完整”的函数,就像一个严重偏科的学生,只对某些科目感兴趣,而对其他科目不感兴趣。内容被丢弃作为封面。PartialFunction不能用“partial”来泛化,所以需要将多个partialfunction组合起来,达到全面覆盖的目的。所以这个PartialFunction确实是一个“偏”函数。比较Function和PartialFunction,比较学术的解释是:对于给定的输入参数类型,函数可以接受该类型的任意值。换句话说,一个(Int)=>String函数可以接受任何Int值并返回一个String。对于给定的输入参数类型,偏函数只能接受该类型的某些值。定义为(Int)=>String的部分函数可能不接受所有Int值作为输入。在Scala中,所有偏函数的类型都定义为PartialFunction[-A,+B]类型,PartialFunction[-A,+B]是从Function1派生的。由于它只处理输入参数的某些分支,因此它使用isDefineAt()来确定输入值是否应由当前部分函数处理。PartialFunction的定义如下:traitPartialFunction[-A,+B]extends(A=>B){self=>importPartialFunction._defisDefinedAt(x:A):BooleandefapplyOrElse[A1<:A,B1>:B](x:A1,default:A1=>B1):B1=if(isDefinedAt(x))apply(x)elsedefault(x)}由于partial函数只处理部分分支,自然可以结合模式匹配。case语句本质上是PartialFunction的子类。当我们定义如下值时:valp:PartialFunction[Int,String]={case1=>"One"}实际上是在创建PartialFunction[Int,String]的子类,其中isDefineAt方法提供了类似这样的实现:defisDefineAt(x:Int):Boolean=x==1当我们通过p(1)调用partial函数时,相当于调用Int=>String函数的apply()方法,从而返回转换后的值“one”。如果传入的参数makeisDifineAt返回false,则会抛出MatchError异常。追根溯源,是因为这里对偏函数值的调用实际上是在调用AbstractPartialFunction的apply()方法(case语句相当于继承了AbstractPartialFunction的子类):abstractclassAbstractPartialFunction[@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]extendsFunction1[T1,R]withPartialFunction[T1,R]{self=>defapply(x:T1):R=applyOrElse(x,PartialFunction.empty)}apply()方法内部调用PartialFunction的applyOrElse()方法.如果isDefineAt(x)返回false,x的值将传递给PartialFunction.empty。这个空值等于PartialFunction[Any,Nothing]类型的值empty_pf,定义如下:x:Any)=thrownewMatchError(x)overridedeforElse[A1,B1](that:PartialFunction[A1,B1])=thatoverridedefandThen[C](k:Nothing=>C)=thisoverridevallift=(x:Any)=>NoneoverridedefrunWith[U](action:Nothing=>U)=constFalse}这就是执行p(2)会抛出MatchError的原因。为什么要使用偏函数?依我拙见,还是粒度复用的问题。函数式编程的思想是用“演绎法”而不是“归纳法”来求解空间。也就是说,不是把问题概括出来,然后分解问题解决问题,而是看透问题的本质,定义最原始的运算和组合规则,面对问题的时候,就可以解决通过组合各种功能来解决问题,这也正是“组合子”的意思。偏函数更进一步,将函数解空间中的各个分支分开,形成一个可以组合的偏函数。偏函数中最常见的组合方法是orElse、andThen和compose。orElse等同于OR运算。如果通过它组合多个偏函数,就相当于形成了一个多案例综合的模式匹配。假设所有偏函数都满足输入值的所有分支,则将它们组合起来形成一个函数。例如,要编写求绝对值的运算,可以使用偏函数:0=>0}valnegativeNumber:PartialFunction[Int,Int]={casexifx<00=>-x}defabs(x:Int):Int={(positiveNumberorElsezeroorElsenegativeNumber)(x)}使用orElse组合时,也可以直接组合case语句,例如:valpf:PartialFunction[Int,String]={caseiifi%2==0=>"even"}valtf:(Int=>String)=pforElse{case_=>"odd"}orElse定义在PartialFunction类型,andThen和Compose一样,不同的是,它们实际上是在Function中定义的,而PartialFunction只是重写了这两个方法。这意味着组合函数可以使用andThen和compose,部分函数也可以使用。这两种方法的作用是将多个(部分)函数组合成一个新的函数,只是组合的顺序不同,而Then是组合第一个,再组合第二个,以此类推;而compose的顺序相反。使用andThen组合偏函数,设计本质接近Pipe-and-Filter模式,每个偏函数都可以理解为一个Filter。因为要将这些偏函数组合起来形成一个流水线,这就要求组合后的偏函数的输入输出值必须支持拼接,即前面的偏函数的输出值会作为输入值的下一个偏函数。与orElse相比,它是不同的。orElse要求所有合并的偏函数必须是同一类型的偏函数定义,比如都是Int=>String,或者String=>CustomizedClass。在PartialFunction中,andThen方法返回一个名为AndThen的偏函数:traitPartialFunction[-A,+B]extends(A=>B){overridedefandThen[C](k:B=>C):PartialFunction[A,C]=newAndThen[A,B,C](this,k)}objectPartialFunction{privateclassAndThen[-A,B,+C](pf:PartialFunction[A,B],k:B=>C)extendsPartialFunction[A,C]{defisDefinedAt(x:A)=pf.isDefinedAt(x)defapply(x:A):C=k(pf(x))overridedefapplyOrElse[A1<:A,C1>:C](x:A1,default:A1=>C1):C1={valz=pf.applyOrElse(x,checkFallback[B])if(!fallbackOccurred(z))k(z)elsedefault(x)}}}注意andThen接收的参数是k:B=>C,即函数类型而不是偏函数类型。当然,由于偏函数继承自函数,所以也可以组合偏函数。如果andThen组合偏函数,入参必须满足所有参与组合的偏函数,否则会抛出MatchError错误。比如写一个函数,需要将字符串中的数字替换成对应的英文单词,可以这样实现:valp1:PartialFunction[String,String]={casesifs.contains("1")=>s。replace("1","one")}valp2:PartialFunction[String,String]={casesifs.contains("2")=>s.replace("2","two")}valp=p1andThenp2如果调用p("123"),返回结果为"onetwo3",但是如果传入p("13"),会抛出MatchError错误,因为p2偏函数的isDefineAt返回false。偏函数可以用在很多场景中。例如,我们可以使用orElse等语义来编写DSL风格的代码,使其更加灵活和可读。本书使用orElse处理金融行业的需求:valforHKG:PartialFunction[Market,List[TaxFee]]=...valforSGP:PartialFunction[Market,List[TaxFee]]=...valforAll:PartialFunction[Market,List[TaxFee]]=...defforTrade(trade:Trade):List[TaxFee]=(forHKGorElseforSGPorElseforAll)(trade.market)也可以有效利用部分函数的开放性,让API调用者可以针对具体的需求场景在它自己的案例陈述中传递。比如Twitter的EffectiveScala给出的case:traitPublisher[T]{defsubscribe(f:PartialFunction[T,Unit])}valpublisher:Publisher[Int]=...publisher.subscribe{caseiifisPrime(i)=>println("foundprime",i)caseiifi%2==0=>count+=2/*ignoretherest*/}AKKA的Actor中定义的receive()方法也是偏函数:traitActor{typeReceive=Actor.Receivedefreceive:Actor.Receive}objectActor{typeReceive=PartialFunction[Any,Unit]}由于部分函数继承自函数,如果一个方法需要接收函数,它也可以接收部分函数。比如我们常用的map、filter等方法可以接收偏函数:valsample=1to10samplemap{casexifx%2==0=>x+"iseven"casexifx%2==1=>x+"isodd"}在推特上有效在Scala中,给出了使用map的编码风格建议://avoidlistmap{item=>itemmatch{caseSome(x)=>xcaseNone=>default}}//recommendlistmap{caseSome(x)=>xcaseNone=>default}本质上,假设list的类型是List[Option[String]],前者以Option[String]=>String的形式传递给map一个函数,后者创建一个PartialFunction[Option[String]的实例,String]传递给地图。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文
