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