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

 http://thomassun.iteye.com/blog/2078407

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

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

 

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

 

 

Java代码   收藏代码
  1. songTitle.map(_.toLowerCase)  
这看上去没啥特别。那么我们来看看这种语法如何用在稍微不同的场景里:我们有一个pair的序列,每一个pair表示一个词以及它在文中出现的频次。我们想要过滤出那些超过或不到一定频次的单词,仅仅返回单词就可以了,不需要单词的词频。我们来写一个函数 wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String]。

 

我们的第一种解决方法是用filter和map方法,用我们熟悉的方式传递一个匿名函数:

 

Java代码   收藏代码
  1. val wordFrequencies = ("habitual"6) :: ("and"56) :: ("consuetudinary"2) ::  
  2.   ("additionally"27) :: ("homely"5) :: ("society"13) :: Nil  
  3. def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =  
  4.   wordFrequencies.filter(wf => wf._2 > 3 && wf._2 < 25).map(_._1)  
  5. wordsWithoutOutliers(wordFrequencies) // List("habitual", "homely", "society")  
这方法存在几个问题,首先是可读性 - 访问tuple的字段我看着都要吐了(_2是啥,_._1又是啥哦)。 只要能够解构这些Tuple,我们的代码就会看着舒服些,当然也会可读些。

 

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

 

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

 

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

 

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

 

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

偏函数

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

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

 

Java代码   收藏代码
  1. 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函数:

 

Java代码   收藏代码
  1. val pf: PartialFunction[(String, Int), String] = {  
  2.   case (word, freq) if freq > 3 && freq < 25 => word  
  3. }  

 

我们为case子句添加了一个守卫分句,所以函数将不会处理不在指定范围的元素。

除了用上面的模式匹配匿名函数的语法外,我们还可以通过显式的继承PartialFunction接口来实现这个函数:

 

Java代码   收藏代码
  1. val pf = new PartialFunction[(String, Int), String] {  
  2.   def apply(wordFrequency: (String, Int)) = wordFrequency match {  
  3.     case (word, freq) if freq > 3 && freq < 25 => word  
  4.   }  
  5.   def isDefinedAt(wordFrequency: (String, Int)) = wordFrequency match {  
  6.     case (word, freq) if freq > 3 && freq < 25 => true  
  7.     case _ => false  
  8.   }  
  9. }  
显然,你可能更愿意用简洁的模式匹配匿名函数的写法。

 

如果我们将这偏函数传递给map方法,编译是可以通过的(译者注:因为偏函数是Function1的子类),不过将会抛出运行时错误MatchError,因为由于守卫分句的存在,这个偏函数没有为所有可能的输入值进行处理:

 

Java代码   收藏代码
  1. wordFrequencies.map(pf) // 将会抛出MatchError  
 当我们将这个偏函数传递给collect方法时,它将会如我们所愿的执行,同时进行过滤和map:

 

 

Java代码   收藏代码
  1. wordFrequencies.collect(pf) // List("habitual", "homely", "society")  
这个结果和我们目前实现的 wordsWithoutOutliers方法输出是一致的。让我们来改写下 wordsWithoutOutliers 函数
Java代码   收藏代码
  1. def wordsWithoutOutliers(wordFrequencies: Seq[(String, Int)]): Seq[String] =  
  2.   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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值