Haskell Tutorial(5)如喝水般自然的高階函式


在〈Haskell Tutorial(4)這裏,那裏,到處都是函式〉中談過,對於 a -> a -> a,將最後一個當成傳回值,而前面就是參數,只是個簡單說法,多參數函式,其實是由多個單參數函式連續呼叫組成,因為這樣的特性,在其他語言中貌似高深的高階函式,在 Haskell 中根本就像喝水一樣自然的應用。

部份套用函式

多參數函式,其實是由多個單參數函式連續呼叫組成,這到底是什麼意思?舉例來說,如果你妽下定義了一個函式:

add :: Int -> Int -> Int
add x y = x + y

那麼你可以使用 add 10 20 這樣的方式來呼叫,結果會是 30,你也可以使用 (add 10) 20 的方式來呼叫,結果也是 30,幹嘛多此一舉地用個括號?你也可以如下方式來呼叫:

部份套用

範例中 (add 10) 看似沒有提供 add 完整的引數,然而有傳回東西,你令 addTen(add 10) 的傳回值,這個傳回值顯然是個函式,因為你可以使用 addTen 20 來呼叫,使用 :t 來檢驗 addTen,確實是個可接受 Int 並傳回 Int 的函式。

這種沒有對多參數函式套用所有引數而取回的函式,稱為部份套用(Partially applied)函式,對於 add 而言,可以將型態 Int -> Int -> Int 看成或寫成是 Int -> (Int -> Int),也就是接受一個引數,然後傳回一個 Int -> Int 的函式,這也就是為何使用 :t 檢驗 addTen 時,結果會是 Int -> Int 的原因,addTen 的效果其實相當於:

addTen :: Int -> Int
addTen y = 10 + y

Curried 函式

對於這種在套用不齊全的引數下,會傳回函式以便後續套用引數的多參數函式,我們稱為 Curried 函式,你也許在其他主流語言中,看過它試圖用奇妙的方式來實現這個特性,然而,這在 Haskell 中,卻是多參數函式本身的組成方式。

Curried 函式的作用在於,你可以隨時隨地,依需求從既有的函式中直接產生新的函式。記得在 Haskell 中,到處都是函式嗎?就連 +-*/ 等都是函式,知道可以幹什麼了嗎?你根本不用定義一個 add 函式,然後再用 addTen 來得到一個可以加 10 的函式,只要如下進行就可以了:

部份套用

+ 本身是個接受兩個引數的函式,(+ 10) 傳回一個函式,這使得 addTen 相當於以下的定義:

addTen :: Integer -> Integer
addTen x = x + 10  

對於 +*/ 等運算,都可以使用 (+ 10) 這樣的方式來得到新函式,- 是個例子,因為 (- 10) 這樣的表示,在 Haskell 是當作負 10,你可以使用 subtract 來得到一個減 10 的函式,像是 subtract 10

高階函式

好吧!你也許也知道,如果一個函式可以接受函式作為引數,或者是傳回函式,或者兩者皆有,這種函式就被稱為高階(High order)函式,所以了,+ 也可以是一個高階函式了,在其他語言中層級高一層的高階函式,在 Haskell 其實很普通。

不過,嚴格來說,這種普通只是在語法層面上簡單而已,高階函式其實通常意謂著,有一個流程模式太常見,因而將共用的流程抽取出來,不能共用需要有特定的部份,可以讓你指定程式碼。

舉例而言,你會想對一個數字清單,過濾出超過 3 的清單,你會走訪各元素,然後對各數字比對是否大於 3,是的話收集到新的清單中,在另一個需求中,你也會想對一個數字清單,比對出是否小於 5,你會走訪各元素,然後對各數字比對是否小於 5,是的話收集到新的清單中 …

單是在上頭的描述中,你就會察覺得重複的文字描述了,實際上,我也只是複製然後改改幾個字,就完成第二個需求描述,也就是只將「大於 3」改為「小於 5」罷了,如果這個運算是可以傳遞的那麼「你也會想對一個數字清單,比對出是否 ???,你會走訪各元素,然後對各數字比對是否 ???,是的話收集到新的清單中 」就可以封裝為一個函式了。

在 Haskell 中,已經有這個函式,叫做 filter,例如:

filter

在 Haskell 中,>< 等也都是函式,(> 3) 建立了一個函式,(< 5)也建立了一個函式,filter的第二個參數接受函式,第三個函式接受一個清單,在 Haskell 中可以使用[]來建立清單,filter` 封裝了走訪各元素的流程,這樣你就可以指定比對條件了。

filter 是個高階函式,(a -> Bool) 是第一個參數接受的型態,也就是一個函式,第二個參數接受 [a] 清單,傳回值也是 [a] 清單。

組合既有的函式

如果現在還沒有清單可以用,你只是想要一個可以比對大於 3 的函式,那該怎麼辦呢?可以如下定義:

biggerThanThree :: [Int] -> [Int]
biggerThanThree xs = filter (> 3) xs

實際上,此時 xs 可以省略,直接令 biggerThanThreefilter (> 3) 就可以了:

biggerThanThree :: [Int] -> [Int]
biggerThanThree = filter (> 3)

這稱為 Point freePointless 風格,實際上將此風格強調出來,就是讓你在面對這類需求時,可以直接思考如何組合既有的函式,產生一個新函式來滿足需求。同樣地,在主流語言中,往往得用奇妙的設計來達成這類需求,在 Haskell 中很簡單,舉個來說,Haskell 中有個 map,如果你有個清單 [10, 20, 30, 40, 50],想對每個都加上 5 後傳回新清單,那就 map (+ 5) [10, 20, 30, 40, 50],傳回結果就是 [15, 25, 35, 45, 55]

如果你只是要個可以對清單全部加 5 的函式呢?那就是組合 map(+ 5) 了,map (+ 5) 就是你想要的:

map

這邊看到了 filtermap 等高階函式,之後有機會還會介紹到其他的,在這篇結束之前,來出個考題好了,請試著用你目前看過的所有函式知識,實作出以下 xsMap 效果:

map