9.1 减少代码重复
所有的函数都能被分解成每次函数调用都一样的公共部分和每次调用不一样的非公共部分。公共部分是函数体,而非公共部分必须通过实参传入。当你把函数值当作入参的时候,这段算法的非公共部分本身又是另一个算法!每当这样的函数被调用,你都可以传入不同的函数值作为实参,被调用的函数会(在由它选择的时机)调用传入的函数值。这些高阶函数(higher-order function),即那些接收函数作为参数的函数,让你有额外的机会来进一步压缩和简化代码。
高阶函数的好处之一是可以用来创建减少代码重复的控制抽象。例如,假定你在编写一个文件浏览器,而你打算提供API给用户来查找匹配某个条件的文件。首先,添加了一个机制用来查找文件名是以指定字符串结尾的文件。比如,这将允许用户查找所有扩展名为“.scala”的文件。可以通过在单例对象中定义一个公共的filesEnding方法的方式来提供这样的API,就像这样:
这个filesEnding方法用私有的助手方法filesHere来获取当前目录下的所有文件,然后基于文件名是否以用户给定的查询条件结尾来过滤这些文件。由于filesHere是私有的,filesEnding方法是FileMatcher(也就是你提供给用户的API)中定义的唯一一个能被访问到的方法。
到目前为止,一切都很完美,暂时都还没有重复的代码。不过到了后来,你决定要让人们可以基于文件名的任意部分进行搜索。当用户记不住他们到底是将文件命名成了phb-important.doc、stupid-phb-report.doc、may2003salesdoc.phb,还是别的什么完全不一样的名字,他们只知道名字中某个地方出现了“phb”,这个时候这样的功能就很有用。于是回去给你的FileMatcher API添加了这个函数:
这个函数跟filesEnding的运行机制没什么两样:搜索filesHere,检查文件名,如果名字匹配则返回文件。唯一的区别是这个函数用的是contains而不是endsWith。
几个月过去了,这个程序变得更成功了。终于,你对某些高级用户提出的想要基于正则表达式搜索文件的请求屈服了。这些喜欢偷懒的用户有着大量拥有上千个文件的巨大目录,他们想做到类似找出所有标题中带有“oopsla”字样的“pdf”文件。为了支持他们,编写了下面这个函数:
有经验的程序员会注意到这些函数中不断重复的代码,有没有办法将它们重构成公共的助手函数呢?按显而易见的方式来并不行。你会想要做到这样的效果:
这种方式在某些动态语言中可以做到,但Scala并不允许像这样在运行时将代码黏在一起。那怎么办呢?
函数值提供了一种答案。虽然不能将方法名像值一样传来传去,但是可以通过传递某个帮你调用方法的函数值来达到同样的效果。在本例中,可以给方法添加一个matcher参数,该参数唯一的目的就是检查文件名是否满足某个查询条件:
在这个版本的方法中,if子句用matcher来检查文件名是否满足查询条件。这个检查具体做什么,取决于给定的matcher。现在,我们来看看matcher这个类型本身。它首先是个函数,因此在类型声明中有个=>符号。这个函数接收两个字符串类型的参数(分别是文件名和查询条件),返回一个布尔值,因此这个函数的完整类型是(String, String) => Boolean。
有了这个新的filesMatching助手方法,可以将前面三个搜索方法进行简化,调用助手方法,传入合适的函数:
本例中展示的函数字面量用的是占位符语法,这个语法在前一章介绍过,可能对你来说还不是非常自然。所以来澄清一下占位符是怎么用的:filesEnding方法里的函数字面量_.endsWith(_)的含义跟下面这段代码是一样的:
由于filesMatching接收一个要求两个String入参的函数,并不需要显式地给出入参的类型,可以直接写(fileName, query) => fileName.endsWith(query)。因为这两个参数在函数体内分别只用到一次(第一个参数fileName先被用到,然后是第二个参数query),可以用占位符语法来写:_.endsWith(_)。第一个下画线是第一个参数(即文件名)的占位符,而第二个下画线是第二个参数(即查询字符串)的占位符。
这段代码已经很简化了,不过实际上还能更短。注意这里的查询字符串被传入filesMatching后,filesMatching并不对它做任何处理,只是将它传入matcher函数。这样的来回传递是不必要的,因为调用者已经知道这个查询字符串了!完全可以将query参数从filesMatching和matcher中移除,这样就得到示例9.1的代码。
示例9.1 用闭包减少代码重复
这个例子展示了一等函数是如何帮助你消除代码重复的,没有它们,我们很难做到这样。比如在Java中,你可能会写一个接口,这个接口包含一个接收String返回Boolean的方法,然后创建并传入一个实现了这个接口的匿名内部类的实例给filesMatching。虽然这种做法能够消除掉重复的代码,但同时它也增加了不少甚至更多新的代码。因此,这样的投入带来的收益并不大,你大可以忍受原先的代码重复。
不仅如此,这个示例还展示了闭包是如何帮助我们减少代码重复的。前一例中我们用到的函数字面量,比如_.endsWith(_)和_.containts(_),都是在运行时被实例化成函数值的,它们并不是闭包,因为它们并不捕获任何自由变量。举例来说,在表达式_.endsWith(_)中用到的两个变量都是由下画线表示的,这意味着它们取自该函数的入参。因此,_.endsWith(_)使用了两个绑定变量,并没有使用任何自由变量。相反,在最新的这个例子中,函数字面量_.endsWith(query)包含了一个绑定变量,即用下画线表示的那一个,和一个名为query的自由变量。正因为Scala支持闭包,你才能在最新的这个例子中将query参数从filesMatching中拿掉,从而更进一步简化代码。