减少Scala中的代码重复

开发 后端
本文节选自Martin Odersky,Lex Spoon和Bill Venners所著,Regular翻译的《Programming in Scala》的第九章。Scala是一种针对 JVM 将函数和面向对象技术组合在一起的编程语言。

所有的函数都被分割成通用部分,它们在每次函数调用中都相同,以及非通用部分,在不同的函数调用中可能会变化。通用部分是函数体,而非通用部分必须由参数提供。当你把函数值用做参数时,算法的非通用部分就是它代表的某些其它算法。在这种函数的每一次调用中,你都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用参数的时候调用传入的函数值。这种高阶函数:higher-order function——带其它函数做参数的函数——给了你额外的机会去组织和简化代码。

51CTO编辑推荐:Scala编程语言专题

高阶函数的一个好处是它们能让你创造控制抽象从而使你减少代码重复。例如,假设你正在写一个文件浏览器,并且你想要提供一个API,能够允许使用者搜索匹配某些标准的文件。首先,你加入了搜索文件名结束于特定字串的机制。这能让你的用户发现,比方说,所有扩展名为“.scala”的文件。你可以通过在单例对象中定义公开的filesEnding方法提供这样的API,如:

  1. object FileMatcher {  
  2.  private def filesHere = (new java.io.File(".")).listFiles  
  3.  def filesEnding(query: String) =  
  4.   for (file < - filesHere; if file.getName.endsWith(query))  
  5.    yield file  
  6. }  
filesEnding方法通过使用私有帮助方法filesHere接受当前目录所有文件的列表,然后基于是否每个文件名以用户特定的查询结尾来过滤它们。由于filesHere是私有的,filesEnding方法是定义在你提供给你用户的API,FilesMatcher中唯一可以访问的方法。

目前为止还挺好,没有重复的代码。然而后来,你决定让别人可以基于文件名的任何部分做查询。这个功能可以良好地用于以下情况:你的用户记不住他们是以phb-important.doc,stupid-pub-report.doc,may2003salesdoc.phb,或什么完全不同的名字来命名文件的,但他们认为“phb”出现在文件的什么地方。你回到工作并把这个函数加到你的API,FileMatcher中:

  1. def filesContaining(query: String) =  
  2.  for (file < - filesHere; if file.getName.contains(query))  
  3.   yield file  
这段函数与filesEnding很像。它搜索filesHere,检查名称,并且如果名称匹配则返回文件。唯一的差别是这个函数使用了contains替代endsWith。

随着时间的推移,程序变得更加成功。最后,你屈服于几个强势用户的需求,他们想要基于正则表达式搜索。这些马虎的家伙拥有数千个文件的超大目录,他们希望能做到像发现所有在题目中什么地方包含“oopsla”的“pdf”文件这样的事。为了支持他们,你写了这个函数:

  1. def filesRegex(query: String) =  
  2.  for (file < - filesHere; if file.getName.matches(query))  
  3.   yield file  
有经验的程序员会注意到所有的这些重复并想知道是否能从中提炼出通用的帮助函数。然而,显而易见的方式不起作用。你希望能做的的是这样的:

  1. def filesMatching(query: String, method) =  
  2.  for (file < - filesHere; if file.getName.method(query))  
  3.   yield file  
这种方式在某些动态语言中能起作用,但Scala不允许在运行期这样粘合代码。那么你该做什么呢?

函数值提供了一个答案。虽然你不能把方法名当作值传递,但你可以通过传递为你调用方法的函数值达到同样的效果。在这个例子里,你可以给方法添加一个matcher参数,其唯一的目的就是针对查询检查文件名:

  1. def filesMatching(query: String,  
  2.   matcher: (String, String) => Boolean) = {  
  3.  for (file < - filesHere; if matcher(file.getName, query))  
  4.   yield file  
  5. }  
方法的这个版本中,if子句现在使用matcher针对查询检查文件名。更精确的说法是这个检查不依赖于matcher定义了什么。现在看一下matcher的类型。它是一个函数,因此类型中有个=>。这个函数带两个字串参数——文件名和查询——并返回布尔值,因此这个函数的类型是(String, String) => Boolean。

有了这个新的filesMatching帮助方法,你可以通过让三个搜索方法调用它,并传入合适的函数来简化它们:

  1. def filesEnding(query: String) =  
  2.  filesMatching(query, _.endsWith(_))  
  3. def filesContaining(query: String) =  
  4.  filesMatching(query, _.contains(_))  
  5. def filesRegex(query: String) =  
  6.  filesMatching(query, _.matches(_))  
这个例子中展示的函数文本使用了前一章中介绍的占位符语法,对你来说可能感觉不是非常自然。因此,以下阐明例子里是如何使用占位符的。用在filesEnding方法里的函数文本_.endsWith(_),与下面的是一回事:

  1. (fileName: String, query: String) => fileName.endsWith(query) 
原因是filesMatching带一个函数,这个函数需要两个String参数,不过你不需要指定参数类型。因此,你也可以写成(fileName, query) => fileName.endsWith(query)。由于第一个参数,fileName,在方法体中被第一个使用,第二个参数,query,第二个使用,你也可以使用占位符语法:_.endsWith(_)。第一个下划线是第一个参数,文件名的占位符,第二个下划线是第二个参数,查询字串的占位符。

代码已经被简化了,但它实际还能更短。注意到query传递给了filesMatching,但filesMatching没有用查询做任何事只是把它传回给传入的matcher函数。这个传来传去的过程不是必需的,因为调用者在前面就已经知道了query的内容。你可以同样从filesMatching和matcher中简单地去除query参数,因此简化后的代码如展示在代码9.1中那样。

  1. object FileMatcher {  
  2.  private def filesHere = (new java.io.File(".")).listFiles  
  3.  private def filesMatching(matcher: String => Boolean) =  
  4.   for (file < - filesHere; if matcher(file.getName))  
  5.    yield file  
  6.  def filesEnding(query: String) =  
  7.   filesMatching(_.endsWith(query))  
  8.  def filesContaining(query: String) =  
  9.   filesMatching(_.contains(query))  
  10.  def filesRegex(query: String) =  
  11.   filesMatching(_.matches(query))  
  12. }  
代码 9.1 使用闭包减少代码重复

这个例子演示了函数作为第一类值帮助你减少代码重复的方式,如果没有它们这将变得很困难。比方说在Java里,你可以创建包括带一个String并返回Boolean的方法的接口,然后创建并传递实现这个接口的匿名内部类实例给filesMatching。尽管这个方式能去除你尝试简化掉的代码重复,但同时它增加了许多乃至更多的新代码。因此好处就不值这个开销了,于是你或许就安于重复代码的现状了。

再者,这个例子还演示了闭包是如何能帮助你减少代码重复的。前面一个例子里用到的函数文本,如_.endsWith(_)和_.contains(_),都是在运行期实例化成函数值而不是闭包,因为它们没有捕获任何自由变量。举例来说表达式_.endsWith(_)里用的两个变量,都是用下划线代表的,也就是说它们都是从传递给函数的参数获得的。因此,_.endsWith(_)使用了两个绑定变量,而不是自由变量。相对的,最近的例子里面用到的函数文本_.endsWith(query),包含一个绑定变量,下划线代表的参数,和一个名为query的自由变量。仅仅因为Scala支持闭包才使得你可以在最近的这个例子里从filesMatching中去掉query参数,从而更进一步简化了代码。

【相关阅读】

  1. Scala:尾递归的跟踪调用及其局限
  2. Scala允许的重复参数
  3. 学习Scala的闭包
  4. Scala的偏应用函数
  5. Scala:函数文本的短格式和占位符语法

责任编辑:book05 来源: Artima
相关推荐

2022-05-22 21:16:46

TypeScriptOmit 工具

2009-07-22 07:43:00

Scala重复参数

2021-04-05 22:38:30

Python操作符代码

2021-09-03 08:21:20

前端代码模块

2017-03-30 10:21:47

jsinspect前端代码库

2009-07-22 07:47:00

Scala客户代码

2009-09-09 14:09:35

Scala Trait

2009-09-22 10:15:42

PersistentQScala

2009-09-22 09:59:40

QueueCollecScala

2009-07-22 07:53:00

Scala扩展类

2009-07-08 15:35:18

Case类Scala

2009-07-22 09:29:44

ScalaSpiral程序

2023-06-12 15:33:52

Scalafor循环语句

2009-09-28 11:42:21

KestrelScala

2022-03-08 14:02:35

GuavaMapjava

2009-09-28 11:25:17

PersistentQKestrelScala

2009-07-21 17:21:57

Scala定义函数

2021-12-16 16:35:46

CSS代码前端

2009-07-08 12:43:59

Scala ServlScala语言

2010-09-14 15:34:41

Scala
点赞
收藏

51CTO技术栈公众号