Scala学习笔记_3 case语句与偏函数

Scala通过case语句提供了形式简单、功能强大的模式匹配功能。但是也许你不知道,Scala还具有一个与case语句相关的语言特性,那就是:在Scala中,被“{}”包含的一系列case语句可以被看成是一个函数字面量,它可以被用在任何普通的函数字面量适用的地方,例如被当做参数传递。

scala> val defaultValue:Option[Int] => Int = {
case  Some(x) => x
case None => 0                                }                                                                    
scala> defaultValue(Some(5))                       
res1: Int = 5

defaultValue是一个函数字面量,它的值是:

{
  case  Some(x) => x
  case None => 0                               
}

看懂了以上的代码,我们就不难理解在Scala的Actor中经常使用的react函数的语法形式:

react {
case (name: String, actor: Actor) => {
  actor ! getip(name)
  act()
}
case msg => {
  println("Unhandled message: "+ msg)
  act()
}
}

react是一个函数,它接收一个函数字面量作为参数。至此我们都没有提到偏函数的概念。什么是偏函数?它与Case语句有什么关系?

在Scala中,偏函数是具有类型PartialFunction[-A,+B]的一种函数。A是其接受的函数类型,B是其返回的结果类型。偏函数最大的特点就是它只接受和处理其参数定义域的一个子集,而对于这个子集之外的参数则抛出运行时异常。这与Case语句的特性非常契合,因为我们在使用case语句是,常常是匹配一组具体的模式,最后用“_”来代表剩余的模式。如果一一组case语句没有涵盖所有的情况,那么这组case语句就可以被看做是一个偏函数。

case语句作为偏函数字面量:

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

second函数的功能是返回一个List[Int]中的第二个值。case函数体只涵盖了当一个List的长度大于2的情况,而忽略Nil和长度为1的列表。

scala.MatchError: List(2)
        at $anonfun$1.apply(<console>:9)
        at $anonfun$1.apply(<console>:9)
        at .<init>(<console>:11)
        at .<clinit>(<console>)
        at RequestResult$.<init>(<console>:9)
        at RequestResult$.<clinit>(<console>)
        at RequestResult$scala_repl_result(<console>)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at scala.tools.nsc.Interpreter$Request$$anonfun$loadAndRun$1$$anonfun$apply$18.apply(Interpreter.scala:981)
        at scala.tools.nsc.Interpreter$Request$$anonfun$loadAndRun$1$$anonfun$apply$18.apply(Interpreter.scala:981)
        at scala.util.contr...

scala> second(1::2::Nil)
res8: Int = 2

当我们试图传入一个不在偏函数的定义域范围内的参数时,抛出了异常。如果我们想在调用函数前先检查一个参数是否在定义域范围以避免抛出异常,那么可以使用偏函数的isDefinedAt方法。

scala> second.isDefinedAt(List(2,3,4))
res10: Boolean = true

实际上,scala编译器把函数字面量:

{
    case List(x::y::_) => y
}

编译成了如下的等价形式:

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
}
}

这种转换是一种编译期行为,我们必须把second显式的声明为PartialFunction类型,如果没有给second指定类型,那么scala编译器会把后面的一组case语句编译成Function1类型,即完整的函数。

Tips:一组case语句要成为一个偏函数,那么它被赋予的变量必须被声明为PartionFunction[-A,+B]

那么我们什么时候该使用偏函数?或者说偏函数给我们带来了什么好处?当我们确定我们的程序不会被传入不可处理的值时,我们就可以使用偏函数。这样万一程序被传入了不应该被传入的值,程序自动抛出异常,而不需要我们手工编写代码去抛出异常,减少了我们的代码量。

相关推荐