天天看點

使用Scala高價函數簡化代碼減少重複代碼柯裡化可變長度參數貸出模式傳名參數 by-name parameter總結

在scala裡,帶有其他函數做參數的函數叫做<code>高階函數</code>,使用高階函數可以簡化代碼。

有這樣一段代碼,查找目前目錄樣以某一個字元串結尾的檔案:

如果,我們想查找包含某一個字元串的檔案,則代碼需要修改為:

上面的改動隻是使用了 contains 替代 endswith,但是随着需求越來越複雜,我們要不停地去修改這段代碼。例如,我想實作正則比對的查找,則代碼會是下面這個樣子:

為了應變複雜的需求,我們可以進行重構代碼,抽象出變化的代碼部分,将其聲明為一個方法:

這樣,針對不同的需求,我們可以編寫不同的matcher方法實作,該方法傳回一個布爾值。

有了這個新的 filesmatching 幫助方法,你可以通過讓三個搜尋方法調用它,并傳入合适的函數 來簡化它們:

上面的例子使用了占位符,例如, filesending 方法裡的函數文本 <code>_.endswith(_)</code> 其實就是:

因為,已經确定了參數類型為字元串,故上面可以省略參數類型。由于第一個參數 filename 在方法體中被第一個使用,第二個參數 query 第二個使用,你也可以使用占位符文法:<code>_.endswith(_)</code>。第一個下劃線是第一個參數檔案名的占位符,第二個下劃線是第二個參數查詢字串的占位符。

因為query參數是從外部傳過來的,其可以直接傳遞給matcher函數,故filesmatching可以隻需要一個參數:

上面的例子使用了函數作為第一類值幫助你減少代碼重複的方式,另外還示範了閉包是如何能幫助你減少代碼重複的。前面一個例子裡用到的函數文本,如 <code>_.endswith(_)</code>和<code>_.contains(_)</code>都是在運作期執行個體化成函數值而不是閉包,因為它們沒有捕 獲任何自由變量。

舉例來說,表達式<code>_.endswith(_)</code>裡用的兩個變量都是用下劃線代表的,也就是說它們都是從傳遞給函數的參數獲得的。是以,<code>_.endswith(_)</code>使用了兩個綁定變量,而不是自由變量。

相對的,最近的例子裡面用到的函數文本<code>_.endswith(query)</code>包含一個綁定變量,下劃線代表的參數和一個名為 query 的自由變量。僅僅因為 scala 支援閉包才使得你可以在最近的這個例子裡從 <code>filesmatching</code> 中去掉 query 參數,進而更進一步簡化了代碼。

另外一個例子,是循環集合時可以使用<code>exists</code>方法來簡化代碼。以下是使用了這種方式的方法去判斷是否傳入的 list 包含了負數的例子:

采用和上面例子同樣的方法,我們可以抽象代碼,将重要的邏輯抽離到一個獨立的方法中去實作。對于上面的查找判斷是否存在的邏輯,scala中提供了高階函數 exists 來實作,代碼如下:

同樣,如果你要查找集合中是否存在偶數,則可以使用下面的代碼:

當函數有多個參數清單時,可以使用<code>柯裡化函數</code>來簡化代碼調用。例如,對下面的函數,它實作兩個 int 型參數,x 和 y 的加法:

我們可以将其柯裡化,代之以一個清單的兩個int參數,實作如下:

當你調用 curriedsum,你實際上背靠背地調用了兩個傳統函數。第一個函數調 用帶單個的名為 x 的 int 參數,并傳回第二個函數的函數值,第二個函數帶 int 參數 y。

你可以使用<code>偏函數</code>,填上第一個參數并且部分應用第二個參數。

<code>curriedsum(1)_</code>裡的下劃線是第二個參數清單的占位符。結果就是指向一個函數的參考,這個函數在被調用的時候,對它唯一的int參數加1并傳回結果:

前面的例子提到了使用函數作為參數,我們可以将這個函數的執行結果再次作為參數傳入函數,即<code>雙倍</code>控制結構:能夠重複一個操作兩次并傳回結果。

下面是一個例子:

上面例子中 op 的類型是 <code>double =&gt; double</code>,就是說它是帶一個 double 做參數并傳回另一個 double 的函數。這裡,op函數等同于:

op函數會執行兩次,第一次是執行<code>add(5)=6</code>,第二次是執行<code>add(add(5))=add(6)=6+1=7</code>。

任何時候,你發現你的代碼中多個地方有重複的代碼塊,你就應該考慮把它實作為這種雙重控制結構。

考慮這樣一種需求:打開一個資源,對它進行操作,然後關閉資源,你可以這樣實作:

有了這個方法,你就可以這樣使用:

注意: 這裡和上面的例子一樣,使用了<code>=&gt;</code> 來映射式定義函數,其可以看成是沒有參數的函數,傳回一個匿名函數;調用的時候是調用這個傳回的匿名函數。

使用這個方法的好處是,調用這個方法隻需要關注如何操作資源,而不用去關心資源的打開和關閉。這個技巧被稱為<code>貸出模式</code>:loan pattern,因為該函數要個模闆方法一樣,實作了資源的打開和關閉,而将使用 printwriter 操作資源<code>貸出</code>給函數,交由調用者來實作。

例子裡的 withprintwriter 把 printwriter 借給函數 op。當函數完成的時候,它發出信号說明它不再需要“借”的資源。于是資源被關閉在 finally 塊中,以确信其确實被關閉,而忽略函數是正常結束傳回還是抛出了異常。

因為,這個函數有兩個參數,是以你可以将該函數柯裡化:

這樣的話,你可以如下方式調用:

這個例子裡,第一個參數清單,包含了一個 file 參數,被寫成包圍在小括号中。第二個參數清單,包含了一個函數參數,被包圍在大括号中。

當一個函數隻有一個參數時,可以使用大括号代替小括号。

《programming in scala》的第九章提到了<code>傳名參數</code>這個概念。其中舉的例子是:實作一個稱為myassert的斷言函數,該函數将帶一個函數值做輸入并參考一個标志位來決定該做什麼。

如果沒有傳名參數,你可以這樣寫myassert:

這個定義是正确的,但使用它會有點兒難看:

你或許很想省略函數文本裡的空參數清單和<code>=&gt;</code>符号,寫成如下形式:

傳名函數恰好為了實作你的願望而出現。要實作一個傳名函數,要定義參數的類型開始于<code>=&gt;</code>而不是<code>() =&gt;</code>。例如,你可以通過改變其類型<code>() =&gt; boolean</code>為<code>=&gt; boolean</code>,把myassert的predicate參數改為傳名參數。

現在你可以在需要斷言的屬性裡省略空的參數了。使用bynameassert的結果看上去就好象使用了内建控制結構:

傳名類型中,空的參數清單<code>()</code>被省略,它僅在參數中被允許。沒有什麼傳名變量或傳名字段這樣的東西。

現在,你或許想知道為什麼你不能簡化myassert的編寫,使用陳舊的boolean作為它參數的類型,如:

當然這種格式同樣合法,并且使用這個版本boolassert的代碼看上去仍然與前面的一樣:

雖然如此,這兩種方式之間存在一個非常重要的差别須指出。因為boolassert的參數類型是boolean,在boolassert(5 &gt; 3)裡括号中的表達式先于boolassert的調用被評估。表達式<code>5 &gt; 3</code>産生true,被傳給boolassert。相對的,因為bynameassert的predicate參數的類型是<code>=&gt; boolean</code>,<code>bynameassert(5 &gt; 3)</code>裡括号中的表達式不是先于bynameassert的調用被評估的。而是代之以先建立一個函數值,其apply方法将評估<code>5 &gt; 3</code>,而這個函數值将被傳遞給bynameassert。

是以這兩種方式之間的差别,在于如果斷言被禁用,你會看到boolassert括号裡的表達式的某些副作用,而bynameassert卻沒有。例如,如果斷言被禁用,boolassert的例子裡嘗試對<code>x / 0 == 0</code>的斷言将産生一個異常:

但在bynameassert的例子裡嘗試同樣代碼的斷言将不産生異常:

本文主要總結了幾種使用scala高階函數簡化代碼的方法,涉及到的知識點有:柯裡化、偏函數、函數映射式定義、可變長度參數、貸出模式以及傳名參數。需要意識到的是,靈活使用高階函數可以簡化代碼,但也可能會增加代碼閱讀的複雜度。