Scala新手指南中文版 - 第四篇 Pattern Matching Anonymous Functions(模式匹配匿名函数)

译者注:原文出处http://danielwestheide.com/blog/2012/12/12/the-neophytes-guide-to-scala-part-4-pattern-matching-anonymous-functions.html,翻译:Thomas

 

前一篇中,我大致介绍了在Scala里使用模式的几种方式,顺便提到了模式还可用在匿名函数中。此篇我们将会深入的理解这个话题,来看看定义匿名函数的不同方式。

如果你参加过Coursera的Scala课程(译者注:还没有参加过?赶紧报名吧)或者已经用Scala一阵子了,你一定已经时不时的写过些匿名函数了。比如,为了搜索索引,你可能想要把一个歌名列表转成小写,你可以写一个匿名函数然后丢给list的map方法:

 

val songTitles = List("The White Hare", "Childe the Hunter", "Take no Rogues")
songTitles.map(t => t.toLowerCase)
或者,你想要代码简短些,你可以用Scala的占位符语法写出下面的规范代码:

 

 

songTitle.map(_.toLowerCase)
这看上去没啥特别。那么我们来看看这种语法如何用在稍微不同的场景里:我们有一个pair的序列,每一个pair表示一个词以及它在文中出现的频次。我们想要过滤出那些超过或不到一定频次的单词,仅仅返回单词就可以了,不需要单词的词频。我们来写一个函数 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")
这方法存在几个问题,首先是可读性 - 访问tuple的字段我看着都要吐了(_2是啥,_._1又是啥哦)。 只要能够解构这些Tuple,我们的代码就会看着舒服些,当然也会可读些。

 

谢天谢地,Scala提供一种定义匿名函数的替代方法:模式匹配匿名函数是由一些case组成的以花括号包含的代码块作为函数体,不过代码块前不带match关键字。我们用此方式来来重写下函数:

 

def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =
  wordFrequencies.filter { case (_, f) => f > 3 && f < 25 } map { case (w, _) => w }
在这个例子中,每个匿名函数只有一个case,因为情况不复杂 - 我们只是单纯的解构一个我们在编译时已经明确的数据结构,所以不会出错。这是使用模式匹配匿名函数的最常用的用法。

 

如果你把两个匿名函数赋给常量,你就会清楚知道它们的实际类型:

 

val predicate: (String, Int) => Boolean = { case (_, f) => f > 3 && f < 25 }
val transformFn: (String, Int) => String = { case (w, _) => w }
请注意你必须提供参数的数据类型,因为Scala编译器没有足够信息来推断模式匹配匿名函数的类型。

 

没人能够阻止你实现非常复杂的case序列,不过,如果你定义类似的匿名函数并且将它们传递给其它函数,像我们的例子一样,你必须要确保针对所有可能的输入,你的匿名函数中必须有一个case能被匹配到并返回值,否则运行时可能抛出MatchError

偏函数

有时候,你仅仅需要能处理特定输入数据范围的的参数函数,事实上这种函数能帮助我们排除目前实现的wordsWithoutOutliers函数的另一个问题:我们首先过滤输入的序列然后map被过滤出来的元素,如果我们可以归结成一个方案使我们只需要遍历序列一次,就能让函数消耗更少的CPU周期,提高执行效率,同时让代码更加短小易读。

如果你看过Scala集合的API,你也许会注意到有一个叫做collect的方法,在一个Seq[A]数据下,它的形式是

 

def collect[B](pf: PartialFunction[A, B])
该方法为Seq里的每个元素执行一次偏函数并返回一个新的序列 - 这偏函数同时过滤和map序列。

 

那么什么是偏函数呢?简单来说,它是一个明确只处理特定数据范围的一元函数,调用者可以检查它是否有定义某个数据的处理逻辑。

为此,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
 当我们将这个偏函数传递给collect方法时,它将会如我们所愿的执行,同时进行过滤和map:

 

 

  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 }
偏函数还有其他一些有用的特性,例如,他们提供了函数链接,用规整的函数式的方法来实现OOP里的所谓责任链模式。这个话题会在后续篇章里讲解,在我们搞定函数拼装问题后。

偏函数也是许多Scala类库和API的要点。例如,Akka的actor定义如何处理发送给它的消息时就是通过定义偏函数的方式。因而,理解偏函数这个概念对理解许多Scala的类库和API显得非常重要。

总结

在此章节中,我们知道了用不同方式定义匿名函数,即一组case语句的方法,它提供了一种更显精简的途径进行漂亮的结构。此外,我们还涉及了偏函数的话题,通过一个简单的使用场景演示了它们强大的用处。

在下一章节,我会带大家深挖一下无所不在的Option类型,解释为什么需要这个类型,如何最好的利用它。

有任何问题或反馈,请让我知道(译者注:有任何翻译错误或问题的讨论,也可以联系我:Thomas

作者:Daniel Westheide,2012.12.12

 

你可能感兴趣的:(scala)