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