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传递过去!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员互动联盟

【专业技术】关于JS的prototype

概述: 在接触JS的过程中,随着理解的深入会逐渐的理解一些比较深奥的理论或者知识,那么今天我们来介绍一下比较难理解的prototype和constructor。...

2906
来自专栏菩提树下的杨过

ruby学习笔记(6)-Array的使用

ruby的数组基本使用,跟c#中的数组比起来,最不习惯的区别在于允许负索引(跟javascript到有几分相似) arr=[3,4,5,6,7,8,9] pu...

1905
来自专栏aCloudDeveloper

重载(overload)、覆盖(override)、隐藏(hide)的区别

这三个概念都是与OO中的多态有关系的。如果单是区别重载与覆盖这两个概念是比较容易的,但是隐藏这一概念却使问题变得有点复杂了,下面说说它们的区别吧。 重载是...

2376
来自专栏移动端开发

Swift2.0 函数学习笔记

最近又有点忙,忙着找工作,忙着适应这个新环境。现在好了,上班两周周了,也适应过来了,又有时间安安静静的就行我们前面的学习了。今天这篇笔记,记录的就是函数的使用。...

1966
来自专栏GreenLeaves

C#运算符的优先级

在C#中,一共有38个常用的运用符,根据它们所执行运算的特点和它们的优先级,为了便于记忆,我将它们归为七个等级:1、单元运算符和括号。2、常规算术运算符。3、位...

21010
来自专栏Kevin-ZhangCG

排序算法总结

1783
来自专栏软件开发 -- 分享 互助 成长

为什么构造函数不能为虚函数

1、从使用角度         虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以...

2049
来自专栏微信公众号:Java团长

Java基础11 对象引用

我们之前一直在使用“对象”这个概念,但没有探讨对象在内存中的具体存储方式。这方面的讨论将引出“对象引用”(object reference)这一重要概念。

812
来自专栏Python爱好者

Java基础笔记08

1255
来自专栏编程

python的函数(一):基本概念

我们之前学了一些基础的过程语句,如if else、while、for。随着我们python程序的功能越来越复杂,代码也就越来越长,因此我们就需要用“函数”来简化...

1898

扫码关注云+社区