Scala之偏函数Partial Function

http://blog.csdn.net/bluishglc/article/details/50995939

从使用case语句构造匿名函数谈起
Scala里,我们可以使用case语句来创建一个匿名函数(函数字面量),这有别于一般的匿名函数创建方法。来看个例子:

scala> List(1,2,3) map {case i:Int=>i+1}
res1: List[Int] = List(2, 3, 4)

这很有趣,case i:Int=>i+1
构建的匿名函数等同于(i:Int)=>i+1
,也就是下面这个样子:

scala> List(1,2,3) map {(i:Int)=>i+1}
res2: List[Int] = List(2, 3, 4)

scala In Programming》一书对独立的case语句作为匿名函数(函数字面量)有权威的解释:

Essentially, a case sequence is a function literal, only more general. Instead of having a single entry point and list of parameters, a case sequence has multiple entry points, each with their own list of parameters. Each case is an entry point to the function, and the parameters are specified with the pattern.

一个case语句就是一个独立的匿名函数,如果有一组case语句的话,从效果上看,构建出的这个匿名函数会有多种不同的参数列表,每一个case对应一种参数列表,参数是case后面的变量声明,其值是通过模式匹配赋予的。
使用case语句构造匿名函数的“额外”好处
使用case语句构造匿名函数是有“额外”好处的,这个“好处”在下面这个例子中得到了充分的体现:

List(1, 3, 5, "seven") map { case i: Int => i + 1 } // won't work
// scala.MatchError: seven (of class java.lang.String)
List(1, 3, 5, "seven") collect { case i: Int => i + 1 }
// verify
assert(List(2, 4, 6) == (List(1, 3, 5, "seven") collect { case i: Int => i + 1 }))

在这个例子中:传递给map的case语句构建的是一个普通的匿名函数,在把这个函数适用于”seven”元素时发生了类型匹配错误。而对于collect,它声明接受的是一个偏函数:PartialFunction,传递的case语句能良好的工作说明这个case语句被编译器自动编译成了一个PartialFunction!这就是case语句“额外”的好处:case语句(组合)除了可以被编译为匿名函数(类型是FunctionX,在Scala里,所有的函数字面量都是一个对象,这个对象的类型是FunctionX),还可以非常方便的编译为一个偏函数PartialFunction!(注意:PartialFunction同时是Function1的子类)编译器会根据调用处的函数类型声明自动帮我们判定如何编译这个case语句(组合)。
上面我们直接抛出了偏函数的概念,这会让人头晕,我们可以只从collect这个示例的效果上去理解偏函数:它只对会作用于指定类型的参数或指定范围值的参数实施计算,超出它的界定范围之外的参数类型和值它会忽略(未必会忽略,这取决于你打算怎样处理)。就像上面例子中一样,case i: Int => i + 1
只声明了对Int参数的处理,在遇到”seven”元素时,不在偏函数的适用范围内,所以这个元素被忽略了。
正式认识偏函数Partial Function
如同在一开始的例子中那样,我们手动实现了一个与case i:Int=>i+1
等价的那个匿名函数(i:Int)=>i+1
,那么在上面的collect方法中使用到的case i: Int => i + 1
它的等价函数是什么呢?显然,不可能是(i:Int)=>i+1
了,因为我们已经解释了,collect接受的参数类型是PartialFunction[Any,Int]
,而不是(Int)=>Int
。 那个case语句对应的偏函数具体是什么样的呢?来看:

scala> val inc = new PartialFunction[Any, Int] {
     | def apply(any: Any) = any.asInstanceOf[Int]+1
     | def isDefinedAt(any: Any) = if (any.isInstanceOf[Int]) true else false
     | }
inc: PartialFunction[Any,Int] = <function1>

scala> List(1, 3, 5, "seven") collect inc
res4: List[Int] = List(2, 4, 6)

PartialFunction特质规定了两个要实现的方法:apply和isDefinedAt,isDefinedAt用来告知调用方这个偏函数接受参数的范围,可以是类型也可以是值,在我们这个例子中我们要求这个inc函数只处理Int型的数据。apply方法用来描述对已接受的值如何处理,在我们这个例子中,我们只是简单的把值+1,注意,非Int型的值已被isDefinedAt方法过滤掉了,所以不用担心类型转换的问题。
上面这个例子写起来真得非常笨拙,和前面的case语句方式比起来真是差太多了。这个例子从反面展示了:通过case语句组合去是实现一个偏函数是多么简洁。实际上case语句组合与偏函数的用意是高度贴合的,所以使用case语句组合是最简单明智的选择,同样是上面的inc函数,换成case去写如下:

scala> def inc: PartialFunction[Any, Int] =
     | { case i: Int => i + 1 }
inc: PartialFunction[Any,Int]

scala> List(1, 3, 5, "seven") collect inc
res5: List[Int] = List(2, 4, 6)

当然,如果偏函数的逻辑非常复杂,可能通过定义一个专门的类并继承PartialFunction是更好选择。

Case语句是如何被编译成偏函数的
关于这个问题在《Programming In Scala》中有较为详细的解释。对于这样一个使用case写在的偏函数:

val second: PartialFunction[List[Int],Int] = {
    case x :: y :: _ => y
}

In fact, such an expression gets ranslated by the Scala compiler to a partial function by translating the patterns twice—once for the implementation of the real function, and once to test whether the function is defined or not. For instance, the function literal { case x :: y :: _ => y }above gets translated to the following partialfunction value:

new PartialFunction[List[Int], Int] {
    def apply(xs: List[Int]) = xs match {
        case x :: y :: _ => y
    }
    def isDefinedAt(xs: List[Int]) = xs match {
        case x :: y :: _ => true
        case _ => false
    }
}

为什么偏函数需要抽象成一个专门的Trait
首先,在Scala里,一切皆对象,函数字面量(匿名函数)也不例外!这也是为什么我们可以把函数字面量赋给一个变量的原因, 是对象就有对应的类型,那么一个函数字面量的真实类型是什么呢?看下面这个例子:

scala> var inc = (x: Int) => x + 1
inc: Int => Int = <function1>

scala> inc.isInstanceOf[Function1[Int,Int]]
res0: Boolean = true

在Scala的scala包里,有一系列Function trait,它们实际上就是函数字面量作为“对象”存在时对应的类型。Function类型有多个版本,Function0表示无参数函数,Function1表示只有一个参数的函数,以此类推。至此我们解释的是一个普遍性问题:是函数就是对象,是对象就有类型。那么,接下来我们看一下偏函数又应该是什么样的一种“类型”?

从语义上讲,偏函数区别于普通函数的唯一特征就是:偏函数会自主地告诉调用方它的处理参数的范围,范围既可是值也可以是类型。针对这样的场景,我们需要给函数安插一种明确的“标识”,告诉编译器:这个函数具有这种特征。所以特质PartialFunction就被创建出来用于“标记”这类函数的,这个特质最主要的方法就是isDefinedAt!同时你也记得PartialFunction还是Function1的子类,所以它也要有apply方法,这是非常自然的,偏函数本身首先是一个函数嘛。
从另一个角度思考,偏函数的逻辑是可以通过普通函数去实现的,只是偏函数是更为优雅的一种方式,同时偏函数特质PartialFunction的存在对调用方和实现方都是一种语义更加丰富的约定,比如collect方法声明使用一个偏函数就暗含着它不太可能对每一个元素进行操作,它的返回结果仅仅是针对偏函数“感兴趣”的元素计算出来的

为什么偏函数只能有一个参数?
为什么只有针对单一参数的偏函数,而不是像Function特质那样,拥有多个版本的PartialFunction呢?在刚刚接触偏函数时,这也让我感到费解,但看透了偏函数的实质之后就会觉得很合理了。我们说所谓的偏函数本质上是由多个case语句组成的针对每一种可能的参数分别进行处理的一种“结构较为特殊”的函数,那特殊在什么地方呢?对,就是case语句,前面我们提到,case语句声明的变量就是偏函数的参数,既然case语句只能声明一个变量,那么偏函数受限于此,也只能有一个参数!说到底,类型PartialFunction无非是为由一组case语句描述的函数字面量提供一个类型描述而已,case语句只接受一个参数,则偏函数的类型声明自然就只有一个参数。
但是,上这并不会对编程造成什么阻碍,如果你想给一个偏函数传递多个参数,完全可以把这些参数封装成一个Tuple传递过去!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,819评论 0 38
  • 函数式编程 引言 Scala中的函数是Java中完全没有的概念。因为Java是完全面向对象的编程语言,没有任何面向...
    义焃阅读 1,279评论 2 5
  • FP 3.1.函数 函数的地位和一般的变量是同等的,可以作为函数的参数,可以作为返回值。传入函数的任何输入是只读的...
    时待吾阅读 1,115评论 0 2
  • 桃花落,闲池阁,山盟虽在,锦书难托,莫,莫,莫! 「函数(Function)」是函数式编程的基本单元。本文将重点讨...
    刘光聪阅读 4,023评论 6 13
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,763评论 2 9