Scala的偏函数 Partial Function-译文

博客原文链接

http://www.cnblogs.com/daoyou/p/3894182.html 

如果你已经用了一段时间scala,那么你了解模式匹配和match/case。就像下面

value match {
  case Some(value) ⇒ …
  case None ⇒ …
}

 

但是,还有一种不带match使用case的情况,比如:

 map foreach { case (k, v) ⇒ println(k + " → " + v) }

第一次遇到的时候,我很疑惑,在哪种情况下,用case可以不用match呢? 实际上,这个带case的代码块是一个匿名函数。(回忆一下scala的匿名函数定义是不是 (f: XXX => XXX))

当然,匿名函数不是什么新玩意,Scala对那些不包含case的代码有非常严格的要求。 但是这种特殊的定义匿名函数的方式给你很多自由,比如类型转换,拆解。 就像上面给出的,case 用在foreach之后,可以很容易的把turple拆解成key和value。

不仅如此,还有很多。请看下面:

  scala> List(41, "cat") map { case i: Int ⇒ i + 1 }
  scala.MatchError: cat (of class java.lang.String)

 就像预期的那样,程序挂掉了,因为这个匹配不知道如何处理字符串cat。

换句话说,这些写就不会出问题:

  scala> List(41, "cat") collect { case i: Int ⇒ i + 1 }
  res1: List[Int] = List(42)

 

那么,到底哪里不同呢? 是因为collect函数捕捉到MatchError以后还是继续吗?如果这样的话实在是太丑陋并且低效了。实际上,这一魔法般的现象的成因是,case所在的代码块实际上定义了特殊的函数--偏函数。

 

现在,你可能会奇怪,源于一个“正常”的语言背景,到底什么是偏函数? 实际上,这个词从数学中来,相对应的是"total",全函数。

但是,即使它(偏函数)从数学中来,它其实是很简单的。我们看下面这个函数

  def inc(i: Int) = i + 1

它可以接受任意的Int型输入。这意味着对于任意的Int型输入,它产生一个Int型的结果。

一个偏函数,只接受所有输入参数的子集(什么意思呢?见函数下面的分析)。

  def fraction(d: Int) = 42 / d

我们都知道,当d = 0,也就是调用fraction(0)的时候,这个函数抛出异常。同样想一想开方函数,并不能接受负值(想到虚数的同学请先站一边看观望)。对于上面的collect函数的例子,也是这样的,那个匿名函数只针对输入是Int型的才有定义,但是String类型的就没有定义。

所以,你是不是想到了,对于一个函数而言,不是所有的输入都有意义。

 

现在,你是不是想到你的程序里面,有非常多这样的例子,很多函数只能在某些输入值的时候才能正常工作。 如果输入一个非法值,函数没法继续执行,返回一个特定的值,或者抛出异常(最好把异常也放在doc说明里)。简言之,偏函数非常常见,即便你不知道它叫这个名字。

所以,这里的fraction被定义成一个常规的函数,用显式的偏函数(partial function)类型。好消息是,Scala内建了偏函数的支持,使用PartialFunction特质。下面就是定义偏函数的例子

  val fraction = new PartialFunction[Int, Int] {

    def apply(d: Int) = 42 / d

    def isDefinedAt(d: Int) = d != 0

}

偏函数一定要提供一个方法是isDefinedAt,让偏函数明白是否能对一个输入给出回复。

  scala> fraction.isDefinedAt(42)
  res2: Boolean = true
  scala> fraction.isDefinedAt(0)
  res3: Boolean = false

如果你这样用:

  scala> fraction(42)
  res4: Int = 1
  scala> fraction(0)
  java.lang.ArithmeticException: / by zero

现在让我们在偏函数中使用case。 上面的fraction函数可以重写成下面的形式。

  val fraction: PartialFunction[Int, Int] = {case d: Int if d != 0 => 42 / d}

然后,如果你这样用:

  scala> fraction(42)
  res5: Int = 1
  scala> fraction(0)
  scala.MatchError: 0 (of class java.lang.Integer)

可以看到,使用case的方式,会抛出一个MatchError。

不仅仅是针对数字,在上面的collect例子里面,偏函数的定义更符合下面的形式:

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

这个函数接受Any作为参数,因为List(41, "cat")是List[Any]类型的。但是,仅仅接受Int才有效。

  scala> incAny(41)
  res6: Int = 42
  scala> incAny("cat")
  scala.MatchError: cat (of class java.lang.String)

传一个String给那个函数,确实不好。但是现在,我们可以在调用之前检查一下:

   scala> incAny.isDefinedAt(41)
  res7: Boolean = true
  scala> incAny.isDefinedAt("cat")
  res8: Boolean = false

所以,我们现在就明白了为什么collect和map,在上面表现的如此不同了。collect函数接收的是一个偏函数。入参41,"cat"都送给这个函数,但是会自动把cat过滤掉。另一个很棒的事情是,scala的编译器能够推断出一个干净的结果类型: List[Int] !(如果只match了Int,当然结果只在Int的那个case里面)

  scala> List(41, "cat") collect incAny
  res9: List[Int] = List(42)

就像你注意到的那样,你可以使用内联的形式定义一个偏函数(就像上面的case),编译器知道那是一个偏函数,所以,尽量避免显式调用PartialFunction特质。

偏函数也会说谎:

  scala> def liar: PartialFunction[Any, Int] =
    { case i: Int ⇒ i; case s: String ⇒ s.toInt }
  liar: PartialFunction[Any,Int]
  scala> liar.isDefinedAt(42)
  res10: Boolean = true
  scala> liar.isDefinedAt("cat")
  res11: Boolean = true
  scala> liar("cat")
  java.lang.NumberFormatException: For input string: "cat"

尽管声称对于输入"cat"有定义,但实际上写成下面的形式更好:

  scala> def honest: PartialFunction[Any, Int] =
      { case i: Int ⇒ i; case s: String if isParsableAsInt(s) ⇒ s.toInt }
  honest: PartialFunction[Any,Int]
  scala> honest.isDefinedAt("cat")
  res12: Boolean = false

现在,你明白偏函数是如何与case结合了的,就像上面用collect的那样,很紧凑。你也会在其他地方看到这样的用法,比如捕获异常。

 

还有一种情况,偏函数只是单纯的偏函数,你可能都不知道它存在。比如下面的这个List:

  val pets = List("cat", "dog", "frog")

在scala中,任何Seq, Set或者Map的实例也是一个函数,所以你可以这样写

  scala> pets(0)
  res13: java.lang.String = cat

但是,

  scala> pets(3)
  java.lang.IndexOutOfBoundsException: 3

是不是说pets函数,只针对0,1,2定义了呢?听起来好熟悉?把pets当做函数看是不是一件很酷的事情?当然可以!因为在scala中, Seq, Set或者Map实际上就是一个偏函数。 所以你可以这样写

  scala> pets.isDefinedAt(0)
  res14: Boolean = true
  scala> pets.isDefinedAt(3)
  res15: Boolean = false

 如果使用一列索引来安全地拿到值,得到一个新的列表,你可以这样写

  scala> Seq(1, 2, 42) collect pets
  res16: Seq[java.lang.String] = List(dog, frog)

collect为我们把异常情况处理的很好。但是,在任何地方都检查isDefinedAt让我们感觉很痛苦。 这很像一个空指针检查,并且,在scala中,我们很讨厌这样。Scala的PartialFunction特质支持lift方法,可以把偏函数转成一个正常的函数,而且可以不Crash。

  scala> pets.lift(0)
  res17: Option[java.lang.String] = Some(cat)
  scala> pets.lift(42)
  res18: Option[java.lang.String] = None

就像你看到的那样, lift方法返回一个Option对象。可以让你安全地处理函数值而不用去做非空(null)检查,不需要你去调用isDefinedAt方法:

  scala> pets.lift(0) map ("I love my " + _) getOrElse ""
  res19: java.lang.String = I love my cat
  scala> pets.lift(42) map ("I love my " + _) getOrElse ""
  res20: java.lang.String = ""

希望这些能帮助你了解到Scala的偏函数。

翻译的原文链接http://blog.bruchez.name/2011/10/scala-partial-functions-without-phd.html

 

posted @ 2014-08-06 13:40  道友慢走  阅读(1635)  评论(0编辑  收藏  举报