译者注:原文出处http://danielwestheide.com/blog/2012/12/12/the-neophytes-guide-to-scala-part-4-pattern-matching-anonymous-functions.html,翻译:Thomas
http://thomassun.iteye.com/blog/2078407
在前一篇中,我大致介绍了在Scala里使用模式的几种方式,顺便提到了模式还可用在匿名函数中。此篇我们将会深入的理解这个话题,来看看定义匿名函数的不同方式。
如果你参加过Coursera的Scala课程(译者注:还没有参加过?赶紧报名吧)或者已经用Scala一阵子了,你一定已经时不时的写过些匿名函数了。比如,为了搜索索引,你可能想要把一个歌名列表转成小写,你可以写一个匿名函数然后丢给list的map方法:
- val songTitles = List("The White Hare", "Childe the Hunter", "Take no Rogues")
- songTitles.map(t => t.toLowerCase)
- songTitle.map(_.toLowerCase)
wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String]。
我们的第一种解决方法是用filter和map方法,用我们熟悉的方式传递一个匿名函数:
- val wordFrequencies = ("habitual", 6) :: ("and", 56) :: ("consuetudinary", 2) ::
- ("additionally", 27) :: ("homely", 5) :: ("society", 13) :: Nil
- def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
- wordFrequencies.filter(wf => wf._2 > 3 && wf._2 < 25).map(_._1)
- wordsWithoutOutliers(wordFrequencies) // List("habitual", "homely", "society")
谢天谢地,Scala提供一种定义匿名函数的替代方法:模式匹配匿名函数是由一些case组成的以花括号包含的代码块作为函数体,不过代码块前不带match关键字。我们用此方式来来重写下函数:
- def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
- wordFrequencies.filter { case (_, f) => f > 3 && f < 25 } map { case (w, _) => w }
如果你把两个匿名函数赋给常量,你就会清楚知道它们的实际类型:
- val predicate: <strong>(String, Int) => Boolean</strong> = { case (_, f) => f > 3 && f < 25 }
- val transformFn: <strong>(String, Int) => String</strong> = { case (w, _) => w }
没人能够阻止你实现非常复杂的case序列,不过,如果你定义类似的匿名函数并且将它们传递给其它函数,像我们的例子一样,你必须要确保针对所有可能的输入,你的匿名函数中必须有一个case能被匹配到并返回值,否则运行时可能抛出MatchError。
偏函数
有时候,你仅仅需要能处理特定输入数据范围的的参数函数,事实上这种函数能帮助我们排除目前实现的wordsWithoutOutliers函数的另一个问题:我们首先过滤输入的序列然后map被过滤出来的元素,如果我们可以归结成一个方案使我们只需要遍历序列一次,就能让函数消耗更少的CPU周期,提高执行效率,同时让代码更加短小易读。
如果你看过Scala集合的API,你也许会注意到有一个叫做collect的方法,在一个Seq[A]数据下,它的形式是
- def collect[B](pf: PartialFunction[A, B])
那么什么是偏函数呢?简单来说,它是一个明确只处理特定数据范围的一元函数,调用者可以检查它是否有定义某个数据的处理逻辑。
为此,PartialFunction提供了一个isDefinedAt方法。实际上,PartialFunction[-A,+B]类型扩展了(A)=>B类型(也可以写作Function1[A,B]),一个模式匹配匿名函数的类型总是PartialFunction。
源于这样的层级关系,在调用一个需要输入Function1作为参数的函数时(如map或filter),给它提供一个模式匹配匿名函数也是完全可以的,只要这函数能够处理所有输入数,也即,总是会匹配一种情形。
而collect方法指定需要传递一个
PartialFunction[A, B]函数,这函数或许不会为所有的输入数据定义处理逻辑。在collect方法内部,它会逐个检查序列中的每个元素,通过调用偏函数的isDefinedAt来确认偏函数里是否定义了相关逻辑,如果isDefinedAt返回false,该元素就会被跳过,否则把元素传递给偏函数,偏函数的返回值被加入collect返回的序列中。
我们先来定义一个偏函数,用来重构wordsWithoutOutliers方法以使用collect函数:
- val pf: PartialFunction[(String, Int), String] = {
- case (word, freq) if freq > 3 && freq < 25 => word
- }
我们为case子句添加了一个守卫分句,所以函数将不会处理不在指定范围的元素。
除了用上面的模式匹配匿名函数的语法外,我们还可以通过显式的继承PartialFunction接口来实现这个函数:
- val pf = new PartialFunction[(String, Int), String] {
- def apply(wordFrequency: (String, Int)) = wordFrequency match {
- case (word, freq) if freq > 3 && freq < 25 => word
- }
- def isDefinedAt(wordFrequency: (String, Int)) = wordFrequency match {
- case (word, freq) if freq > 3 && freq < 25 => true
- case _ => false
- }
- }
如果我们将这偏函数传递给map方法,编译是可以通过的(译者注:因为偏函数是Function1的子类),不过将会抛出运行时错误MatchError,因为由于守卫分句的存在,这个偏函数没有为所有可能的输入值进行处理:
- wordFrequencies.map(pf) // 将会抛出MatchError
- wordFrequencies.collect(pf) // List("habitual", "homely", "society")
wordsWithoutOutliers方法输出是一致的。让我们来改写下
wordsWithoutOutliers
函数:
- def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
- wordFrequencies.collect { case (word, freq) if freq > 3 && freq < 25 => word }
偏函数也是许多Scala类库和API的要点。例如,Akka的actor定义如何处理发送给它的消息时就是通过定义偏函数的方式。因而,理解偏函数这个概念对理解许多Scala的类库和API显得非常重要。
总结
在此章节中,我们知道了用不同方式定义匿名函数,即一组case语句的方法,它提供了一种更显精简的途径进行漂亮的结构。此外,我们还涉及了偏函数的话题,通过一个简单的使用场景演示了它们强大的用处。
在下一章节,我会带大家深挖一下无所不在的Option类型,解释为什么需要这个类型,如何最好的利用它。
有任何问题或反馈,请让我知道(译者注:有任何翻译错误或问题的讨论,也可以联系我:Thomas)
作者:Daniel Westheide,2012.12.12