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