所有的函數(shù)都被分割成通用部分,它們在每次函數(shù)調(diào)用中都相同,以及非通用部分,在不同的函數(shù)調(diào)用中可能會變化,
減少Scala中的代碼重復
。通用部分是函數(shù)體,而非通用部分必須由參數(shù)提供。當你把函數(shù)值用做參數(shù)時,算法的非通用部分就是它代表的某些其它算法。在這種函數(shù)的每一次調(diào)用中,你都可以把不同的函數(shù)值作為參數(shù)傳入,于是被調(diào)用函數(shù)將在每次選用參數(shù)的時候調(diào)用傳入的函數(shù)值。這種高階函數(shù):higher-order function——帶其它函數(shù)做參數(shù)的函數(shù)——給了你額外的機會去組織和簡化代碼。高階函數(shù)的一個好處是它們能讓你創(chuàng)造控制抽象從而使你減少代碼重復。例如,假設(shè)你正在寫一個文件瀏覽器,并且你想要提供一個API,能夠允許使用者搜索匹配某些標準的文件。首先,你加入了搜索文件名結(jié)束于特定字串的機制。這能讓你的用戶發(fā)現(xiàn),比方說,所有擴展名為“.scala”的文件。你可以通過在單例對象中定義公開的filesEnding方法提供這樣的API,如:
object FileMatcher {
private def filesHere = (new java.io.File(".")).listFiles
def filesEnding(query: String) =
for (file < - filesHere; if file.getName.endsWith(query))
yield file
}
filesEnding方法通過使用私有幫助方法filesHere接受當前目錄所有文件的列表,然后基于是否每個文件名以用戶特定的查詢結(jié)尾來過濾它們。由于filesHere是私有的,filesEnding方法是定義在你提供給你用戶的API,F(xiàn)ilesMatcher中唯一可以訪問的方法。
目前為止還挺好,沒有重復的代碼。然而后來,你決定讓別人可以基于文件名的任何部分做查詢。這個功能可以良好地用于以下情況:你的用戶記不住他們是以phb-important.doc,stupid-pub-report.doc,may2003salesdoc.phb,或什么完全不同的名字來命名文件的,但他們認為“phb”出現(xiàn)在文件的什么地方。你回到工作并把這個函數(shù)加到你的API,F(xiàn)ileMatcher中:
def filesContaining(query: String) =
for (file < - filesHere; if file.getName.contains(query))
yield file
這段函數(shù)與filesEnding很像。它搜索filesHere,檢查名稱,并且如果名稱匹配則返回文件。唯一的差別是這個函數(shù)使用了contains替代endsWith。
隨著時間的推移,程序變得更加成功。最后,你屈服于幾個強勢用戶的需求,他們想要基于正則表達式搜索。這些馬虎的家伙擁有數(shù)千個文件的超大目錄,他們希望能做到像發(fā)現(xiàn)所有在題目中什么地方包含“oopsla”的“pdf”文件這樣的事。為了支持他們,你寫了這個函數(shù):
def filesRegex(query: String) =
for (file < - filesHere; if file.getName.matches(query))
yield file
有經(jīng)驗的程序員會注意到所有的這些重復并想知道是否能從中提煉出通用的幫助函數(shù)。然而,顯而易見的方式不起作用。你希望能做的的是這樣的:
def filesMatching(query: String, method) =
for (file < - filesHere; if file.getName.method(query))
yield file
這種方式在某些動態(tài)語言中能起作用,但Scala不允許在運行期這樣粘合代碼。那么你該做什么呢?
函數(shù)值提供了一個答案。雖然你不能把方法名當作值傳遞,但你可以通過傳遞為你調(diào)用方法的函數(shù)值達到同樣的效果。在這個例子里,你可以給方法添加一個matcher參數(shù),其唯一的目的就是針對查詢檢查文件名:
def filesMatching(query: String,
matcher: (String, String) => Boolean) = {
for (file < - filesHere; if matcher(file.getName, query))
yield file
}
方法的這個版本中,if子句現(xiàn)在使用matcher針對查詢檢查文件名,
電腦資料
《減少Scala中的代碼重復》(http://www.oriental01.com)。更精確的說法是這個檢查不依賴于matcher定義了什么,F(xiàn)在看一下matcher的類型。它是一個函數(shù),因此類型中有個=>。這個函數(shù)帶兩個字串參數(shù)——文件名和查詢——并返回布爾值,因此這個函數(shù)的類型是(String, String) => Boolean。有了這個新的filesMatching幫助方法,你可以通過讓三個搜索方法調(diào)用它,并傳入合適的函數(shù)來簡化它們:
def filesEnding(query: String) =
filesMatching(query, _.endsWith(_))
def filesContaining(query: String) =
filesMatching(query, _.contains(_))
def filesRegex(query: String) =
filesMatching(query, _.matches(_))
這個例子中展示的函數(shù)文本使用了前一章中介紹的占位符語法,對你來說可能感覺不是非常自然。因此,以下闡明例子里是如何使用占位符的。用在filesEnding方法里的函數(shù)文本_.endsWith(_),與下面的是一回事:
(fileName: String, query: String) => fileName.endsWith(query)
原因是filesMatching帶一個函數(shù),這個函數(shù)需要兩個String參數(shù),不過你不需要指定參數(shù)類型。因此,你也可以寫成(fileName, query) => fileName.endsWith(query)。由于第一個參數(shù),fileName,在方法體中被第一個使用,第二個參數(shù),query,第二個使用,你也可以使用占位符語法:_.endsWith(_)。第一個下劃線是第一個參數(shù),文件名的占位符,第二個下劃線是第二個參數(shù),查詢字串的占位符。
代碼已經(jīng)被簡化了,但它實際還能更短。注意到query傳遞給了filesMatching,但filesMatching沒有用查詢做任何事只是把它傳回給傳入的matcher函數(shù)。這個傳來傳去的過程不是必需的,因為調(diào)用者在前面就已經(jīng)知道了query的內(nèi)容。你可以同樣從filesMatching和matcher中簡單地去除query參數(shù),因此簡化后的代碼如展示在代碼9.1中那樣。
object FileMatcher {
private def filesHere = (new java.io.File(".")).listFiles
private def filesMatching(matcher: String => Boolean) =
for (file < - filesHere; if matcher(file.getName))
yield file
def filesEnding(query: String) =
filesMatching(_.endsWith(query))
def filesContaining(query: String) =
filesMatching(_.contains(query))
def filesRegex(query: String) =
filesMatching(_.matches(query))
}
代碼 9.1 使用閉包減少代碼重復
這個例子演示了函數(shù)作為第一類值幫助你減少代碼重復的方式,如果沒有它們這將變得很困難。比方說在Java里,你可以創(chuàng)建包括帶一個String并返回Boolean的方法的接口,然后創(chuàng)建并傳遞實現(xiàn)這個接口的匿名內(nèi)部類實例給filesMatching。盡管這個方式能去除你嘗試簡化掉的代碼重復,但同時它增加了許多乃至更多的新代碼。因此好處就不值這個開銷了,于是你或許就安于重復代碼的現(xiàn)狀了。
再者,這個例子還演示了閉包是如何能幫助你減少代碼重復的。前面一個例子里用到的函數(shù)文本,如_.endsWith(_)和_.contains(_),都是在運行期實例化成函數(shù)值而不是閉包,因為它們沒有捕獲任何自由變量。舉例來說表達式_.endsWith(_)里用的兩個變量,都是用下劃線代表的,也就是說它們都是從傳遞給函數(shù)的參數(shù)獲得的。因此,_.endsWith(_)使用了兩個綁定變量,而不是自由變量。相對的,最近的例子里面用到的函數(shù)文本_.endsWith(query),包含一個綁定變量,下劃線代表的參數(shù),和一個名為query的自由變量。僅僅因為Scala支持閉包才使得你可以在最近的這個例子里從filesMatching中去掉query參數(shù),從而更進一步簡化了代碼。